diff --git a/Makefile b/Makefile index 194a6347a..aa3f85563 100644 --- a/Makefile +++ b/Makefile @@ -138,6 +138,7 @@ endif book.build: mdbook build book/ $(if $(call eq,$(out),),,-d $(out)) + rm -rf $(or $(out),book/_rendered)/lib.rs # Spellcheck Book. diff --git a/book/src/types/enums.md b/book/src/types/enums.md index ac2bd85fd..ab7c7641e 100644 --- a/book/src/types/enums.md +++ b/book/src/types/enums.md @@ -70,7 +70,7 @@ enum Episode { # # fn main() {} ``` -> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `camelCase` and `none` (disables any renaming). +> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `snake_case`, `camelCase` and `none` (disables any renaming). ### Documentation and deprecation diff --git a/book/src/types/input_objects.md b/book/src/types/input_objects.md index 318722d58..c416a9412 100644 --- a/book/src/types/input_objects.md +++ b/book/src/types/input_objects.md @@ -14,7 +14,7 @@ In [Juniper], defining a [GraphQL input object][0] is quite straightforward and #[derive(GraphQLInputObject)] struct Coordinate { latitude: f64, - longitude: f64 + longitude: f64, } struct Root; @@ -32,19 +32,40 @@ impl Root { # fn main() {} ``` +[`@oneOf`] [input objects][0] could be defined by using the [`#[derive(GraphQLInputObject)]` attribute][2] on a [Rust enum][enum]: +```rust +# #![expect(unused_variables, reason = "example")] +# extern crate juniper; +# use juniper::{GraphQLInputObject, ID}; +# +#[derive(GraphQLInputObject)] +enum UserBy { + Id(ID), // Every `enum` variant declares a `Null`able input object field, + Name(String), // so there is no need to use `Option` explicitly. +} +# +# fn main() {} +``` + ### Renaming -Just as with [defining GraphQL objects](objects/index.md#renaming), by default [struct] fields are converted from [Rust]'s standard `snake_case` naming convention into [GraphQL]'s `camelCase` convention: +Just as with [defining GraphQL objects](objects/index.md#renaming), by default [struct] fields (or [enum] variants) are converted from [Rust]'s standard naming convention into [GraphQL]'s `camelCase` convention: ```rust # extern crate juniper; -# use juniper::GraphQLInputObject; +# use juniper::{GraphQLInputObject, ID}; # #[derive(GraphQLInputObject)] struct Person { first_name: String, // exposed as `firstName` in GraphQL schema last_name: String, // exposed as `lastName` in GraphQL schema } + +#[derive(GraphQLInputObject)] +enum UserBy { + Id(ID), // exposed as `id` in GraphQL schema + Name(String), // exposed as `name` in GraphQL schema +} # # fn main() {} ``` @@ -52,7 +73,7 @@ struct Person { We can override the name by using the `#[graphql(name = "...")]` attribute: ```rust # extern crate juniper; -# use juniper::GraphQLInputObject; +# use juniper::{GraphQLInputObject, ID}; # #[derive(GraphQLInputObject)] #[graphql(name = "WebPerson")] // now exposed as `WebPerson` in GraphQL schema @@ -62,6 +83,14 @@ struct Person { #[graphql(name = "websiteURL")] website_url: Option, // now exposed as `websiteURL` in GraphQL schema } + +#[derive(GraphQLInputObject)] +#[graphql(name = "By")] // now exposed as `By` in GraphQL schema +enum UserBy { + #[graphql(name = "ID")] + Id(ID), // now exposed as `ID` in GraphQL schema + Name(String), +} # # fn main() {} ``` @@ -69,7 +98,7 @@ struct Person { Or provide a different renaming policy for all the [struct] fields: ```rust # extern crate juniper; -# use juniper::GraphQLInputObject; +# use juniper::{GraphQLInputObject, ID}; # #[derive(GraphQLInputObject)] #[graphql(rename_all = "none")] // disables any renaming @@ -78,10 +107,17 @@ struct Person { age: i32, website_url: Option, // exposed as `website_url` in GraphQL schema } + +#[derive(GraphQLInputObject)] +#[graphql(rename_all = "none")] // disables any renaming +enum UserBy { + Id(ID), // exposed as `Id` in GraphQL schema + Name(String), // exposed as `Name` in GraphQL schema +} # # fn main() {} ``` -> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `camelCase` and `none` (disables any renaming). +> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `snake_case`, `camelCase` and `none` (disables any renaming). ### Documentation and deprecation @@ -89,7 +125,7 @@ struct Person { Similarly, [GraphQL input fields][1] may also be [documented][7] and [deprecated][9] via `#[graphql(description = "...")]` and `#[graphql(deprecated = "...")]`/[`#[deprecated]`][13] attributes: ```rust # extern crate juniper; -# use juniper::GraphQLInputObject; +# use juniper::{GraphQLInputObject, ID}; # /// This doc comment is visible only in Rust API docs. #[derive(GraphQLInputObject)] @@ -112,6 +148,28 @@ struct Person { #[deprecated] another: Option, // has no description in GraphQL schema } + +/// This doc comment is visible only in Rust API docs. +#[derive(GraphQLInputObject)] +#[graphql(description = "This description is visible only in GraphQL schema.")] +enum UserBy { + /// This doc comment is visible only in Rust API docs. + #[graphql(desc = "This description is visible only in GraphQL schema.")] + // ^^^^ shortcut for a `description` argument + Id(ID), + + /// This doc comment is visible in both Rust API docs and GraphQL schema + /// descriptions. + // `enum` variants represent `Null`able input fields already, so can be naturally + // deprecated without any default values. + #[graphql(deprecated = "Just because.")] + Name(String), + + // If no explicit deprecation reason is provided, + // then the default "No longer supported" one is used. + #[deprecated] + Bio(String), // has no description in GraphQL schema +} # # fn main() {} ``` @@ -120,11 +178,11 @@ struct Person { ### Ignoring -By default, all [struct] fields are included into the generated [GraphQL input object][0] type. To prevent inclusion of a specific field annotate it with the `#[graphql(ignore)]` attribute: +By default, all [struct] fields (or [enum] variants) are included into the generated [GraphQL input object][0] type. To prevent inclusion of a specific field/variant annotate it with the `#[graphql(ignore)]` attribute: > **WARNING**: Ignored fields must either implement `Default` or be annotated with the `#[graphql(default = )]` argument. ```rust # extern crate juniper; -# use juniper::GraphQLInputObject; +# use juniper::{GraphQLInputObject, ID}; # enum System { Cartesian, @@ -146,6 +204,15 @@ struct Point2D { // ^^^^ alternative naming, up to your preference shift: f64, } + +#[derive(GraphQLInputObject)] +enum UserBy { + Id(ID), + // Ignored `enum` variants naturally doesn't require `Default` implementation or + // `default` value being specified, as they're just never constructed from an input. + #[graphql(ignore)] + Name(String), +} # # fn main() {} ``` @@ -154,6 +221,9 @@ struct Point2D { + +[`@oneOf`]: https://spec.graphql.org/September2025#sec--oneOf +[enum]: https://doc.rust-lang.org/stable/reference/items/enumerations.html [GraphQL]: https://graphql.org [Juniper]: https://docs.rs/juniper [Rust]: https://www.rust-lang.org diff --git a/book/src/types/interfaces.md b/book/src/types/interfaces.md index 0443d2cf0..318f75c5f 100644 --- a/book/src/types/interfaces.md +++ b/book/src/types/interfaces.md @@ -312,7 +312,7 @@ trait Person { # # fn main() {} ``` -> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `camelCase` and `none` (disables any renaming). +> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `snake_case`, `camelCase` and `none` (disables any renaming). ### Documentation and deprecation diff --git a/book/src/types/objects/complex_fields.md b/book/src/types/objects/complex_fields.md index 82596e9dc..57447bb4b 100644 --- a/book/src/types/objects/complex_fields.md +++ b/book/src/types/objects/complex_fields.md @@ -111,7 +111,7 @@ impl Person { # # fn main() {} ``` -> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `camelCase` and `none` (disables any renaming). +> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `snake_case`, `camelCase` and `none` (disables any renaming). ### Documentation and deprecation diff --git a/book/src/types/objects/index.md b/book/src/types/objects/index.md index 85c590ea0..837430a99 100644 --- a/book/src/types/objects/index.md +++ b/book/src/types/objects/index.md @@ -123,7 +123,7 @@ struct Person { # # fn main() {} ``` -> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `camelCase` and `none` (disables any renaming). +> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `snake_case`, `camelCase` and `none` (disables any renaming). ### Deprecation diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index b03e1c9d1..ad02e14b6 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -24,7 +24,11 @@ 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], [#1062], [#1055], [graphql/graphql-spec#825]) + - `@oneOf` built-in directive. + - `__Type.isOneOf` field. ([#1348]) + - `schema::meta::InputObjectMeta::is_one_of` field. + - `enum`s support to `#[derive(GraphQLInputObject)]` macro. - `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. @@ -37,6 +41,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi - 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]) +- Support of `#[graphql(rename_all = "snake_case")]` attribute in macros. ([#1354]) ### Changed @@ -50,10 +55,13 @@ All user visible changes to `juniper` crate will be documented in this file. Thi - Incorrect double escaping in `ScalarToken::String` `Display`ing. ([#1349]) [#864]: /../../issues/864 +[#1055]: /../../issues/1055 +[#1062]: /../../issues/1062 [#1347]: /../../issues/1347 [#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/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, ) } 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(..) 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. diff --git a/juniper/src/tests/introspection_tests.rs b/juniper/src/tests/introspection_tests.rs index 017196458..c9714f0b6 100644 --- a/juniper/src/tests/introspection_tests.rs +++ b/juniper/src/tests/introspection_tests.rs @@ -1,18 +1,18 @@ use std::collections::HashSet; -use pretty_assertions::assert_eq; - use super::schema_introspection::*; use crate::{ - ScalarValue as _, graphql, + ScalarValue as _, Value, graphql, introspection::IntrospectionFormat, schema::model::RootNode, tests::fixtures::starwars::schema::{Database, Query}, types::scalars::{EmptyMutation, EmptySubscription}, }; +use pretty_assertions::assert_eq; #[tokio::test] async fn test_introspection_query_type_name() { + // language=GraphQL let doc = r#" query IntrospectionQueryTypeQuery { __schema { @@ -46,6 +46,7 @@ async fn test_introspection_query_type_name() { #[tokio::test] async fn test_introspection_type_name() { + // language=GraphQL let doc = r#" query IntrospectionQueryTypeQuery { __type(name: "Droid") { @@ -74,6 +75,7 @@ async fn test_introspection_type_name() { #[tokio::test] async fn test_introspection_specific_object_type_name_and_kind() { + // language=GraphQL let doc = r#" query IntrospectionDroidKindQuery { __type(name: "Droid") { @@ -105,6 +107,7 @@ async fn test_introspection_specific_object_type_name_and_kind() { #[tokio::test] async fn test_introspection_specific_interface_type_name_and_kind() { + // language=GraphQL let doc = r#" query IntrospectionDroidKindQuery { __type(name: "Character") { @@ -136,6 +139,7 @@ async fn test_introspection_specific_interface_type_name_and_kind() { #[tokio::test] async fn test_introspection_documentation() { + // language=GraphQL let doc = r#" query IntrospectionDroidDescriptionQuery { __type(name: "Droid") { @@ -167,6 +171,7 @@ async fn test_introspection_documentation() { #[tokio::test] async fn test_introspection_directives() { + // language=GraphQL let q = r#" query IntrospectionQuery { __schema { @@ -189,7 +194,7 @@ async fn test_introspection_directives() { .await .unwrap(); - let expected = graphql_value!({ + let expected: Value = graphql_value!({ "__schema": { "directives": [ { @@ -209,6 +214,12 @@ async fn test_introspection_directives() { "INLINE_FRAGMENT", ], }, + { + "name": "oneOf", + "locations": [ + "INPUT_OBJECT", + ], + }, { "name": "skip", "locations": [ @@ -227,11 +238,15 @@ async fn test_introspection_directives() { }, }); - assert_eq!(result, (expected, vec![])); + assert_eq!( + serde_json::to_string_pretty(&result.0).unwrap(), + serde_json::to_string_pretty(&expected).unwrap(), + ); } #[tokio::test] async fn test_introspection_possible_types() { + // language=GraphQL let doc = r#" query IntrospectionDroidDescriptionQuery { __type(name: "Character") { diff --git a/juniper/src/tests/schema_introspection.rs b/juniper/src/tests/schema_introspection.rs index e8dc19edc..20e44d081 100644 --- a/juniper/src/tests/schema_introspection.rs +++ b/juniper/src/tests/schema_introspection.rs @@ -1536,6 +1536,15 @@ pub(crate) fn schema_introspection_result() -> Value { } ] }, + { + "name": "oneOf", + "description": null, + "isRepeatable": false, + "locations": [ + "INPUT_OBJECT" + ], + "args": [] + }, { "name": "skip", "description": null, @@ -3014,6 +3023,14 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value { } ] }, + { + "name": "oneOf", + "isRepeatable": false, + "locations": [ + "INPUT_OBJECT" + ], + "args": [] + }, { "name": "skip", "isRepeatable": false, diff --git a/juniper/src/types/utilities.rs b/juniper/src/types/utilities.rs index b19c6ee66..6c49ff39b 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,26 @@ 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 { + 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 .iter() .filter_map(|f| { @@ -176,14 +191,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/juniper_codegen/CHANGELOG.md b/juniper_codegen/CHANGELOG.md index e61e702d5..928f59798 100644 --- a/juniper_codegen/CHANGELOG.md +++ b/juniper_codegen/CHANGELOG.md @@ -11,15 +11,22 @@ All user visible changes to `juniper_codegen` crate will be documented in this f ### Added - [September 2025] GraphQL spec: ([#1347]) + - `@oneOf` input objects: ([#1354], [#1062], [#1055], [graphql/graphql-spec#825]) + - `enum`s support to `#[derive(GraphQLInputObject)]` macro. - 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. - Placing `#[graphql(deprecated)]` attribute on method arguments in `#[graphql_object]` and `#[graphql_interface]` macros. +- Support of `#[graphql(rename_all = "snake_case")]` attribute. ([#1354]) [#864]: /../../issues/864 +[#1055]: /../../issues/1055 +[#1062]: /../../issues/1062 [#1347]: /../../issues/1347 [#1348]: /../../pull/1348 +[#1354]: /../../pull/1354 [graphql/graphql-spec#525]: https://github.com/graphql/graphql-spec/pull/525 [graphql/graphql-spec#805]: https://github.com/graphql/graphql-spec/pull/805 +[graphql/graphql-spec#825]: https://github.com/graphql/graphql-spec/pull/825 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_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 f7137aedf..a4a00f4bb 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,73 @@ 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()?; + + let ignored = field_attr.ignore.is_some(); + if let Some(default) = &field_attr.default { + ERR.emit_custom( + default.span_ident(), + if ignored { + "`default` attribute argument has no meaning for ignored variants, as they are \ + never constructed" + } else { + "field cannot have default value in `@oneOf` input object" + }, + ); + } + + let ident = &v.ident; + + let name = field_attr + .name + .map_or_else( + || { + let mut name = ident.unraw().to_string(); + if renaming != rename::Policy::None { + // Make naming similar to struct fields before applying further renaming. + name = rename::Policy::SnakeCase.apply(&ident.unraw().to_string()); + } + renaming.apply(&name) + }, + SpanContainer::into_inner, + ) + .into_boxed_str(); + if !is_internal && name.starts_with("__") { + ERR.no_double_underscore(v.span()); + } + + let field_ty = v.fields.iter().next().unwrap().ty.clone(); + + Some(FieldDefinition { + ident: ident.clone(), + ty: parse_quote! { ::core::option::Option<#field_ty> }, + default: None, + name, + description: field_attr.description.map(SpanContainer::into_inner), + deprecated: field_attr.deprecated.map(SpanContainer::into_inner), + ignored, + }) +} + +/// 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..f7bfe1fe1 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}, @@ -417,6 +418,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 { @@ -499,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; @@ -540,6 +549,7 @@ impl Definition { registry .build_input_object_type::<#ident #ty_generics>(info, &fields) #description + #one_of .into_meta() } } @@ -617,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_n(&none_pat, n); + let post_none_pats = iter::repeat_n(&none_pat, 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>; @@ -672,9 +720,7 @@ impl Definition { ::std::format!("Expected input object, found: {}", value)) )?; - ::core::result::Result::Ok(#ident { - #( #fields )* - }) + ::core::result::Result::Ok(#body) } } } @@ -694,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 @@ -739,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] @@ -772,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),*]; } } } diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 935d803b7..edb47a928 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -116,40 +116,46 @@ use proc_macro::TokenStream; use self::common::diagnostic::{self, ResultExt as _}; -/// `#[derive(GraphQLInputObject)]` macro for deriving a -/// [GraphQL input object][0] implementation for a Rust struct. Each -/// non-ignored field type must itself be [GraphQL input object][0] or a -/// [GraphQL scalar][2]. +/// `#[derive(GraphQLInputObject)]` macro for deriving a [GraphQL input object][0] implementation +/// for a Rust struct. /// -/// The `#[graphql]` helper attribute is used for configuring the derived -/// implementation. Specifying multiple `#[graphql]` attributes on the same -/// definition is totally okay. They all will be treated as a single attribute. +/// Each non-ignored field type must itself be either a [GraphQL input object][0] or a +/// [GraphQL scalar][2]. [`@oneOf`] [input objects][0] are supported by deriving on a Rust enum. +/// +/// The `#[graphql]` helper attribute is used for configuring the derived implementation. Specifying +/// multiple `#[graphql]` attributes on the same definition is totally okay. They all will be +/// treated as a single attribute. /// /// ```rust -/// use juniper::GraphQLInputObject; +/// # use juniper::{GraphQLInputObject, ID}; /// /// #[derive(GraphQLInputObject)] /// struct Point2D { /// x: f64, /// y: f64, /// } +/// +/// #[derive(GraphQLInputObject)] +/// enum UserBy { // `@oneOF` input objects are defined as `enum`s. +/// Id(ID), // Every `enum` variant declares a `Null`able input object field, +/// Name(String), // so there is no need to use `Option` explicitly. +/// } /// ``` /// /// # Custom name, description and deprecation /// -/// The name of a [GraphQL input object][0] or its [fields][1] may be overridden -/// with the `name` attribute's argument. By default, a type name or a struct -/// field name is used in a `camelCase`. +/// The name of a [GraphQL input object][0] or its [fields][1] may be overridden with the `name` +/// attribute's argument. By default, a type name or a struct field (or enum variant) name is used +/// in a `camelCase`. /// -/// The description of a [GraphQL input object][0] or its [fields][1] may be -/// specified either with the `description`/`desc` attribute's argument, or with -/// a regular Rust doc comment. +/// The description of a [GraphQL input object][0] or its [fields][1] may be specified either with +/// the `description`/`desc` attribute's argument, or with a regular Rust doc comment. /// /// [GraphQL input object fields][1] may be deprecated by specifying the `deprecated` attribute's /// argument, or with the regular Rust `#[deprecated]` attribute. /// /// ```rust -/// # use juniper::GraphQLInputObject; +/// # use juniper::{GraphQLInputObject, ID}; /// # /// #[derive(GraphQLInputObject)] /// #[graphql( @@ -174,22 +180,44 @@ use self::common::diagnostic::{self, ResultExt as _}; /// #[deprecated] /// z: Option, // has no description in GraphQL schema /// } +/// +/// #[derive(GraphQLInputObject)] +/// #[graphql( +/// // Rename the type for GraphQL by specifying the name here. +/// name = "By", +/// // You may also specify a description here. +/// // If present, doc comments will be ignored. +/// desc = "Selector for searching `User`s.", +/// )] +/// enum UserBy { +/// /// ID for exact `User` search. +/// Id(ID), +/// +/// #[graphql(name = "username", desc = "Name for fuzzy search.")] +/// // `enum` variants cannot use `default` attribute's argument, +/// // as it's meaningless for `@oneOf` input objects. +/// #[graphql(deprecated = "Obsolete")] +/// Name(String), +/// +/// // If no explicit deprecation reason is provided, +/// // then the default "No longer supported" one is used. +/// #[deprecated] +/// Bio(String), // has no description in GraphQL schema +/// } /// ``` /// /// # Renaming policy /// -/// By default, all [GraphQL input object fields][1] are renamed in a -/// `camelCase` manner (so a `y_coord` Rust struct field becomes a -/// `yCoord` [value][1] in GraphQL schema, and so on). This complies with -/// default GraphQL naming conventions as [demonstrated in spec][0]. +/// By default, all [GraphQL input object fields][1] are renamed in a `camelCase` manner (so, a +/// `y_coord` Rust struct field becomes a `yCoord` [input field][1] in GraphQL schema, and so on). +/// This complies with the default GraphQL naming conventions as [demonstrated in spec][0]. /// -/// However, if you need for some reason another naming convention, it's -/// possible to do so by using the `rename_all` attribute's argument. At the -/// moment, it supports the following policies only: `SCREAMING_SNAKE_CASE`, -/// `camelCase`, `none` (disables any renaming). +/// However, if you need for some reason another naming convention, it's possible to do so by using +/// the `rename_all` attribute's argument. At the moment, it supports the following policies only: +/// `SCREAMING_SNAKE_CASE`, `snake_case`, `camelCase`, `none` (disables any renaming). /// /// ```rust -/// # use juniper::GraphQLInputObject; +/// # use juniper::{GraphQLInputObject, ID}; /// # /// #[derive(GraphQLInputObject)] /// #[graphql(rename_all = "none")] // disables renaming @@ -197,16 +225,23 @@ use self::common::diagnostic::{self, ResultExt as _}; /// x: f64, /// y_coord: f64, // will be `y_coord` instead of `yCoord` in GraphQL schema /// } +/// +/// #[derive(GraphQLInputObject)] +/// #[graphql(rename_all = "none")] // disables renaming +/// enum UserBy { +/// Id(ID), // will be `Id` instead of `id` in GraphQL schema +/// Name(String), // will be `Name` instead of `name` in GraphQL schema +/// } /// ``` /// /// # Ignoring fields /// -/// To omit exposing a Rust field in a GraphQL schema, use the `ignore` -/// attribute's argument directly on that field. Ignored fields must implement -/// [`Default`] or have the `default = ` attribute's argument. +/// To omit exposing a Rust struct field (or an enum variant) in a GraphQL schema, use the `ignore` +/// attribute's argument directly on it. Ignored struct fields must implement [`Default`] or have +/// the `default = ` attribute's argument. /// /// ```rust -/// # use juniper::GraphQLInputObject; +/// # use juniper::{GraphQLInputObject, ID}; /// # /// enum System { /// Cartesian, @@ -228,8 +263,18 @@ use self::common::diagnostic::{self, ResultExt as _}; /// // ^^^^ alternative naming, up to your preference /// shift: f64, /// } +/// +/// #[derive(GraphQLInputObject)] +/// enum UserBy { +/// Id(ID), +/// // Ignored `enum` variants naturally doesn't require `Default` implementation or +/// // `default` value being specified, as they're just never constructed from an input. +/// #[graphql(ignore)] +/// Name(String), +/// } /// ``` /// +/// [`@oneOf`]: https://spec.graphql.org/September2025#sec--oneOf /// [`ScalarValue`]: juniper::ScalarValue /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects /// [1]: https://spec.graphql.org/October2021#InputFieldsDefinition @@ -309,7 +354,7 @@ pub fn derive_input_object(input: TokenStream) -> TokenStream { /// /// However, if you need for some reason another naming convention, it's /// possible to do so by using the `rename_all` attribute's argument. At the -/// moment, it supports the following policies only: `SCREAMING_SNAKE_CASE`, +/// moment, it supports the following policies only: `SCREAMING_SNAKE_CASE`, `snake_case`, /// `camelCase`, `none` (disables any renaming). /// /// ```rust @@ -1201,7 +1246,7 @@ pub fn derive_scalar_value(input: TokenStream) -> TokenStream { /// /// However, if you need for some reason apply another naming convention, it's /// possible to do by using `rename_all` attribute's argument. At the moment it -/// supports the following policies only: `SCREAMING_SNAKE_CASE`, `camelCase`, +/// supports the following policies only: `SCREAMING_SNAKE_CASE`, `snake_case`, `camelCase`, /// `none` (disables any renaming). /// /// ```rust @@ -1518,7 +1563,7 @@ pub fn derive_interface(body: TokenStream) -> TokenStream { /// /// However, if you need for some reason apply another naming convention, it's /// possible to do by using `rename_all` attribute's argument. At the moment it -/// supports the following policies only: `SCREAMING_SNAKE_CASE`, `camelCase`, +/// supports the following policies only: `SCREAMING_SNAKE_CASE`, `snake_case`, `camelCase`, /// `none` (disables any renaming). /// /// ``` @@ -1749,7 +1794,7 @@ pub fn derive_object(body: TokenStream) -> TokenStream { /// /// However, if you need for some reason apply another naming convention, it's /// possible to do by using `rename_all` attribute's argument. At the moment it -/// supports the following policies only: `SCREAMING_SNAKE_CASE`, `camelCase`, +/// supports the following policies only: `SCREAMING_SNAKE_CASE`, `snake_case`, `camelCase`, /// `none` (disables any renaming). /// /// ``` diff --git a/tests/codegen/fail/input_object/derive_wrong_item.rs b/tests/codegen/fail/input_object/derive_wrong_item.rs new file mode 100644 index 000000000..103ebeaca --- /dev/null +++ b/tests/codegen/fail/input_object/derive_wrong_item.rs @@ -0,0 +1,6 @@ +use juniper::GraphQLInputObject; + +#[derive(GraphQLInputObject)] +union Test { id: i32 } + +fn main() {} diff --git a/tests/codegen/fail/input_object/derive_wrong_item.stderr b/tests/codegen/fail/input_object/derive_wrong_item.stderr new file mode 100644 index 000000000..6ad2cd92c --- /dev/null +++ b/tests/codegen/fail/input_object/derive_wrong_item.stderr @@ -0,0 +1,5 @@ +error: GraphQL input object cannot be derived on unions + --> fail/input_object/derive_wrong_item.rs:4:1 + | +4 | union Test { id: i32 } + | ^^^^^ diff --git a/tests/codegen/fail/input_object/enum/derive_all_variants_ignored.rs b/tests/codegen/fail/input_object/enum/derive_all_variants_ignored.rs new file mode 100644 index 000000000..2e8c36dcf --- /dev/null +++ b/tests/codegen/fail/input_object/enum/derive_all_variants_ignored.rs @@ -0,0 +1,9 @@ +use juniper::GraphQLInputObject; + +#[derive(GraphQLInputObject)] +enum Object { + #[graphql(ignore)] + Field(String), +} + +fn main() {} diff --git a/tests/codegen/fail/input_object/enum/derive_all_variants_ignored.stderr b/tests/codegen/fail/input_object/enum/derive_all_variants_ignored.stderr new file mode 100644 index 000000000..6ed21291e --- /dev/null +++ b/tests/codegen/fail/input_object/enum/derive_all_variants_ignored.stderr @@ -0,0 +1,5 @@ +error: GraphQL input object expected at least 1 non-ignored field + --> fail/input_object/enum/derive_all_variants_ignored.rs:5:5 + | +5 | #[graphql(ignore)] + | ^ diff --git a/tests/codegen/fail/input_object/enum/derive_default_value.rs b/tests/codegen/fail/input_object/enum/derive_default_value.rs new file mode 100644 index 000000000..eee074331 --- /dev/null +++ b/tests/codegen/fail/input_object/enum/derive_default_value.rs @@ -0,0 +1,9 @@ +use juniper::GraphQLInputObject; + +#[derive(GraphQLInputObject)] +enum Object { + #[graphql(default)] + Test(String), +} + +fn main() {} diff --git a/tests/codegen/fail/input_object/enum/derive_default_value.stderr b/tests/codegen/fail/input_object/enum/derive_default_value.stderr new file mode 100644 index 000000000..5d6c3a42b --- /dev/null +++ b/tests/codegen/fail/input_object/enum/derive_default_value.stderr @@ -0,0 +1,6 @@ +error: GraphQL input object field cannot have default value in `@oneOf` input object + · note: https://spec.graphql.org/October2021#sec-Input-Objects + --> fail/input_object/enum/derive_default_value.rs:5:15 + | +5 | #[graphql(default)] + | ^^^^^^^ diff --git a/tests/codegen/fail/input_object/enum/derive_ignored_with_default_value.rs b/tests/codegen/fail/input_object/enum/derive_ignored_with_default_value.rs new file mode 100644 index 000000000..a88450519 --- /dev/null +++ b/tests/codegen/fail/input_object/enum/derive_ignored_with_default_value.rs @@ -0,0 +1,12 @@ +use juniper::GraphQLInputObject; + +#[derive(GraphQLInputObject)] +enum Object { + Num(i32), + #[graphql(ignore, default = "none")] + Test(String), + #[graphql(skip, default)] + Test2(String), +} + +fn main() {} diff --git a/tests/codegen/fail/input_object/enum/derive_ignored_with_default_value.stderr b/tests/codegen/fail/input_object/enum/derive_ignored_with_default_value.stderr new file mode 100644 index 000000000..0ff592365 --- /dev/null +++ b/tests/codegen/fail/input_object/enum/derive_ignored_with_default_value.stderr @@ -0,0 +1,13 @@ +error: GraphQL input object `default` attribute argument has no meaning for ignored variants, as they are never constructed + · note: https://spec.graphql.org/October2021#sec-Input-Objects + --> fail/input_object/enum/derive_ignored_with_default_value.rs:6:23 + | +6 | #[graphql(ignore, default = "none")] + | ^^^^^^^ + +error: GraphQL input object `default` attribute argument has no meaning for ignored variants, as they are never constructed + · note: https://spec.graphql.org/October2021#sec-Input-Objects + --> fail/input_object/enum/derive_ignored_with_default_value.rs:8:21 + | +8 | #[graphql(skip, default)] + | ^^^^^^^ diff --git a/tests/codegen/fail/input_object/enum/derive_incompatible_field_type.rs b/tests/codegen/fail/input_object/enum/derive_incompatible_field_type.rs new file mode 100644 index 000000000..3f6489432 --- /dev/null +++ b/tests/codegen/fail/input_object/enum/derive_incompatible_field_type.rs @@ -0,0 +1,13 @@ +use juniper::{GraphQLInputObject, GraphQLObject}; + +#[derive(GraphQLObject)] +struct ObjectA { + test: String, +} + +#[derive(GraphQLInputObject)] +enum Object { + Field(ObjectA), +} + +fn main() {} diff --git a/tests/codegen/fail/input_object/enum/derive_incompatible_field_type.stderr b/tests/codegen/fail/input_object/enum/derive_incompatible_field_type.stderr new file mode 100644 index 000000000..4ea9f968e --- /dev/null +++ b/tests/codegen/fail/input_object/enum/derive_incompatible_field_type.stderr @@ -0,0 +1,84 @@ +error[E0277]: the trait bound `ObjectA: IsInputType<__S>` is not satisfied + --> fail/input_object/enum/derive_incompatible_field_type.rs:8:10 + | +8 | #[derive(GraphQLInputObject)] + | ^^^^^^^^^^^^^^^^^^ the trait `IsInputType<__S>` is not implemented for `ObjectA` + | + = help: the following other types implement trait `IsInputType`: + `&T` implements `IsInputType` + `Arc` implements `IsInputType` + `ArcStr` implements `IsInputType<__S>` + `Box` implements `IsInputType` + `ID` implements `IsInputType<__S>` + `Object` implements `IsInputType<__S>` + `TypeKind` implements `IsInputType<__S>` + `Vec` implements `IsInputType` + and $N others + = note: required for `std::option::Option` to implement `IsInputType<__S>` + = note: this error originates in the derive macro `GraphQLInputObject` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `ObjectA: FromInputValue<__S>` is not satisfied + --> fail/input_object/enum/derive_incompatible_field_type.rs:8:10 + | +8 | #[derive(GraphQLInputObject)] + | ^^^^^^^^^^^^^^^^^^ the trait `FromInputValue<__S>` is not implemented for `ObjectA` + | + = help: the following other types implement trait `FromInputValue`: + `Arc` implements `FromInputValue` + `ArcStr` implements `FromInputValue<__S>` + `Box` implements `FromInputValue` + `ID` implements `FromInputValue<__S>` + `Object` implements `FromInputValue<__S>` + `TypeKind` implements `FromInputValue<__S>` + `Vec` implements `FromInputValue` + `[T; N]` implements `FromInputValue` + and $N others + = note: required for `std::option::Option` to implement `FromInputValue<__S>` +note: required by a bound in `Registry::::arg` + --> $WORKSPACE/juniper/src/executor/mod.rs + | + | pub fn arg(&mut self, name: impl Into, info: &T::TypeInfo) -> Argument + | --- required by a bound in this associated function + | where + | T: GraphQLType + FromInputValue, + | ^^^^^^^^^^^^^^^^^ required by this bound in `Registry::::arg` + = note: this error originates in the derive macro `GraphQLInputObject` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `ObjectA: FromInputValue<__S>` is not satisfied + --> fail/input_object/enum/derive_incompatible_field_type.rs:8:10 + | +8 | #[derive(GraphQLInputObject)] + | ^^^^^^^^^^^^^^^^^^ the trait `FromInputValue<__S>` is not implemented for `ObjectA` + | + = help: the following other types implement trait `FromInputValue`: + `Arc` implements `FromInputValue` + `ArcStr` implements `FromInputValue<__S>` + `Box` implements `FromInputValue` + `ID` implements `FromInputValue<__S>` + `Object` implements `FromInputValue<__S>` + `TypeKind` implements `FromInputValue<__S>` + `Vec` implements `FromInputValue` + `[T; N]` implements `FromInputValue` + and $N others + = note: this error originates in the derive macro `GraphQLInputObject` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `ObjectA: ToInputValue<_>` is not satisfied + --> fail/input_object/enum/derive_incompatible_field_type.rs:8:10 + | +8 | #[derive(GraphQLInputObject)] + | ^^^^^^^^^^^^^^^^^^ the trait `ToInputValue<_>` is not implemented for `ObjectA` + | + = help: the following other types implement trait `ToInputValue`: + `&T` implements `ToInputValue` + `Arc` implements `ToInputValue` + `ArcStr` implements `ToInputValue<__S>` + `Box` implements `ToInputValue` + `ID` implements `ToInputValue<__S>` + `Object` implements `ToInputValue<__S>` + `TypeKind` implements `ToInputValue<__S>` + `Value` implements `ToInputValue` + and $N others + = note: required for `&ObjectA` to implement `ToInputValue<_>` + = note: 1 redundant requirement hidden + = note: required for `std::option::Option<&ObjectA>` to implement `ToInputValue<_>` + = note: this error originates in the derive macro `GraphQLInputObject` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/codegen/fail/input_object/enum/derive_no_fields.rs b/tests/codegen/fail/input_object/enum/derive_no_fields.rs new file mode 100644 index 000000000..d08ee3e2c --- /dev/null +++ b/tests/codegen/fail/input_object/enum/derive_no_fields.rs @@ -0,0 +1,6 @@ +use juniper::GraphQLInputObject; + +#[derive(GraphQLInputObject)] +enum Object {} + +fn main() {} diff --git a/tests/codegen/fail/input_object/enum/derive_no_fields.stderr b/tests/codegen/fail/input_object/enum/derive_no_fields.stderr new file mode 100644 index 000000000..ebe6d62cd --- /dev/null +++ b/tests/codegen/fail/input_object/enum/derive_no_fields.stderr @@ -0,0 +1,7 @@ +error: GraphQL input object expected at least 1 non-ignored field + --> fail/input_object/enum/derive_no_fields.rs:3:10 + | +3 | #[derive(GraphQLInputObject)] + | ^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `GraphQLInputObject` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/codegen/fail/input_object/enum/derive_no_underscore.rs b/tests/codegen/fail/input_object/enum/derive_no_underscore.rs new file mode 100644 index 000000000..6db1b1a27 --- /dev/null +++ b/tests/codegen/fail/input_object/enum/derive_no_underscore.rs @@ -0,0 +1,9 @@ +use juniper::GraphQLInputObject; + +#[derive(GraphQLInputObject)] +enum Object { + #[graphql(name = "__test")] + Test(String), +} + +fn main() {} diff --git a/tests/codegen/fail/input-object/derive_no_underscore.stderr b/tests/codegen/fail/input_object/enum/derive_no_underscore.stderr similarity index 84% rename from tests/codegen/fail/input-object/derive_no_underscore.stderr rename to tests/codegen/fail/input_object/enum/derive_no_underscore.stderr index ebaa93225..8bab718cb 100644 --- a/tests/codegen/fail/input-object/derive_no_underscore.stderr +++ b/tests/codegen/fail/input_object/enum/derive_no_underscore.stderr @@ -1,6 +1,6 @@ error: All types and directives defined within a schema must not have a name which begins with `__` (two underscores), as this is used exclusively by GraphQL’s introspection system. · note: https://spec.graphql.org/October2021#sec-Schema - --> fail/input-object/derive_no_underscore.rs:5:5 + --> fail/input_object/enum/derive_no_underscore.rs:5:5 | 5 | #[graphql(name = "__test")] | ^ diff --git a/tests/codegen/fail/input_object/enum/derive_unique_name.rs b/tests/codegen/fail/input_object/enum/derive_unique_name.rs new file mode 100644 index 000000000..6b4418c7d --- /dev/null +++ b/tests/codegen/fail/input_object/enum/derive_unique_name.rs @@ -0,0 +1,10 @@ +use juniper::GraphQLInputObject; + +#[derive(GraphQLInputObject)] +enum Object { + Test(String), + #[graphql(name = "test")] + Test2(String), +} + +fn main() {} diff --git a/tests/codegen/fail/input_object/enum/derive_unique_name.stderr b/tests/codegen/fail/input_object/enum/derive_unique_name.stderr new file mode 100644 index 000000000..400974bc4 --- /dev/null +++ b/tests/codegen/fail/input_object/enum/derive_unique_name.stderr @@ -0,0 +1,5 @@ +error: GraphQL input object expected all fields to have unique names + --> fail/input_object/enum/derive_unique_name.rs:5:5 + | +5 | Test(String), + | ^^^^ diff --git a/tests/codegen/fail/input_object/struct/derive_all_fields_ignored.rs b/tests/codegen/fail/input_object/struct/derive_all_fields_ignored.rs new file mode 100644 index 000000000..82880a003 --- /dev/null +++ b/tests/codegen/fail/input_object/struct/derive_all_fields_ignored.rs @@ -0,0 +1,9 @@ +use juniper::GraphQLInputObject; + +#[derive(GraphQLInputObject)] +struct Object { + #[graphql(ignore)] + field: String, +} + +fn main() {} diff --git a/tests/codegen/fail/input_object/struct/derive_all_fields_ignored.stderr b/tests/codegen/fail/input_object/struct/derive_all_fields_ignored.stderr new file mode 100644 index 000000000..1712c6a51 --- /dev/null +++ b/tests/codegen/fail/input_object/struct/derive_all_fields_ignored.stderr @@ -0,0 +1,9 @@ +error: GraphQL input object expected at least 1 non-ignored field + --> fail/input_object/struct/derive_all_fields_ignored.rs:4:15 + | +4 | struct Object { + | _______________^ +5 | | #[graphql(ignore)] +6 | | field: String, +7 | | } + | |_^ diff --git a/tests/codegen/fail/input-object/derive_field_non_deprecable.rs b/tests/codegen/fail/input_object/struct/derive_field_non_deprecable.rs similarity index 100% rename from tests/codegen/fail/input-object/derive_field_non_deprecable.rs rename to tests/codegen/fail/input_object/struct/derive_field_non_deprecable.rs diff --git a/tests/codegen/fail/input-object/derive_field_non_deprecable.stderr b/tests/codegen/fail/input_object/struct/derive_field_non_deprecable.stderr similarity index 89% rename from tests/codegen/fail/input-object/derive_field_non_deprecable.stderr rename to tests/codegen/fail/input_object/struct/derive_field_non_deprecable.stderr index 2959ef204..d092b6667 100644 --- a/tests/codegen/fail/input-object/derive_field_non_deprecable.stderr +++ b/tests/codegen/fail/input_object/struct/derive_field_non_deprecable.stderr @@ -1,5 +1,5 @@ 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 - --> fail/input-object/derive_field_non_deprecable.rs:6:11 + --> fail/input_object/struct/derive_field_non_deprecable.rs:6:11 | 6 | test: String, | ^^^^^^ evaluation of `>::mark::_` failed here @@ -7,7 +7,7 @@ error[E0080]: evaluation panicked: field `test` of `Object` input object cannot = 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) 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 - --> fail/input-object/derive_field_non_deprecable.rs:8:12 + --> fail/input_object/struct/derive_field_non_deprecable.rs:8:12 | 8 | other: i32, | ^^^ evaluation of `>::mark::_` failed here diff --git a/tests/codegen/fail/input-object/derive_incompatible_field_type.rs b/tests/codegen/fail/input_object/struct/derive_incompatible_field_type.rs similarity index 100% rename from tests/codegen/fail/input-object/derive_incompatible_field_type.rs rename to tests/codegen/fail/input_object/struct/derive_incompatible_field_type.rs diff --git a/tests/codegen/fail/input-object/derive_incompatible_field_type.stderr b/tests/codegen/fail/input_object/struct/derive_incompatible_field_type.stderr similarity index 92% rename from tests/codegen/fail/input-object/derive_incompatible_field_type.stderr rename to tests/codegen/fail/input_object/struct/derive_incompatible_field_type.stderr index a16d4240a..111d90b8c 100644 --- a/tests/codegen/fail/input-object/derive_incompatible_field_type.stderr +++ b/tests/codegen/fail/input_object/struct/derive_incompatible_field_type.stderr @@ -1,5 +1,5 @@ error[E0277]: the trait bound `ObjectA: IsInputType<__S>` is not satisfied - --> fail/input-object/derive_incompatible_field_type.rs:10:12 + --> fail/input_object/struct/derive_incompatible_field_type.rs:10:12 | 10 | field: ObjectA, | ^^^^^^^ the trait `IsInputType<__S>` is not implemented for `ObjectA` @@ -16,7 +16,7 @@ error[E0277]: the trait bound `ObjectA: IsInputType<__S>` is not satisfied and $N others error[E0277]: the trait bound `ObjectA: FromInputValue<__S>` is not satisfied - --> fail/input-object/derive_incompatible_field_type.rs:10:12 + --> fail/input_object/struct/derive_incompatible_field_type.rs:10:12 | 8 | #[derive(GraphQLInputObject)] | ------------------ required by a bound introduced by this call @@ -44,7 +44,7 @@ note: required by a bound in `Registry::::arg` | ^^^^^^^^^^^^^^^^^ required by this bound in `Registry::::arg` error[E0277]: the trait bound `ObjectA: FromInputValue<__S>` is not satisfied - --> fail/input-object/derive_incompatible_field_type.rs:8:10 + --> fail/input_object/struct/derive_incompatible_field_type.rs:8:10 | 8 | #[derive(GraphQLInputObject)] | ^^^^^^^^^^^^^^^^^^ the trait `FromInputValue<__S>` is not implemented for `ObjectA` @@ -62,7 +62,7 @@ error[E0277]: the trait bound `ObjectA: FromInputValue<__S>` is not satisfied = note: this error originates in the derive macro `GraphQLInputObject` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `ObjectA: ToInputValue<_>` is not satisfied - --> fail/input-object/derive_incompatible_field_type.rs:8:10 + --> fail/input_object/struct/derive_incompatible_field_type.rs:8:10 | 8 | #[derive(GraphQLInputObject)] | ^^^^^^^^^^^^^^^^^^ the trait `ToInputValue<_>` is not implemented for `ObjectA` diff --git a/tests/codegen/fail/input-object/derive_no_fields.rs b/tests/codegen/fail/input_object/struct/derive_no_fields.rs similarity index 100% rename from tests/codegen/fail/input-object/derive_no_fields.rs rename to tests/codegen/fail/input_object/struct/derive_no_fields.rs diff --git a/tests/codegen/fail/input-object/derive_no_fields.stderr b/tests/codegen/fail/input_object/struct/derive_no_fields.stderr similarity index 67% rename from tests/codegen/fail/input-object/derive_no_fields.stderr rename to tests/codegen/fail/input_object/struct/derive_no_fields.stderr index 4ab046c55..caeae822f 100644 --- a/tests/codegen/fail/input-object/derive_no_fields.stderr +++ b/tests/codegen/fail/input_object/struct/derive_no_fields.stderr @@ -1,5 +1,5 @@ error: GraphQL input object expected at least 1 non-ignored field - --> fail/input-object/derive_no_fields.rs:4:15 + --> fail/input_object/struct/derive_no_fields.rs:4:15 | 4 | struct Object {} | ^^ diff --git a/tests/codegen/fail/input-object/derive_no_underscore.rs b/tests/codegen/fail/input_object/struct/derive_no_underscore.rs similarity index 100% rename from tests/codegen/fail/input-object/derive_no_underscore.rs rename to tests/codegen/fail/input_object/struct/derive_no_underscore.rs diff --git a/tests/codegen/fail/input_object/struct/derive_no_underscore.stderr b/tests/codegen/fail/input_object/struct/derive_no_underscore.stderr new file mode 100644 index 000000000..e9ee38151 --- /dev/null +++ b/tests/codegen/fail/input_object/struct/derive_no_underscore.stderr @@ -0,0 +1,6 @@ +error: All types and directives defined within a schema must not have a name which begins with `__` (two underscores), as this is used exclusively by GraphQL’s introspection system. + · note: https://spec.graphql.org/October2021#sec-Schema + --> fail/input_object/struct/derive_no_underscore.rs:5:5 + | +5 | #[graphql(name = "__test")] + | ^ diff --git a/tests/codegen/fail/input-object/derive_unique_name.rs b/tests/codegen/fail/input_object/struct/derive_unique_name.rs similarity index 100% rename from tests/codegen/fail/input-object/derive_unique_name.rs rename to tests/codegen/fail/input_object/struct/derive_unique_name.rs diff --git a/tests/codegen/fail/input-object/derive_unique_name.stderr b/tests/codegen/fail/input_object/struct/derive_unique_name.stderr similarity index 79% rename from tests/codegen/fail/input-object/derive_unique_name.stderr rename to tests/codegen/fail/input_object/struct/derive_unique_name.stderr index 20a72cfa1..f3815df31 100644 --- a/tests/codegen/fail/input-object/derive_unique_name.stderr +++ b/tests/codegen/fail/input_object/struct/derive_unique_name.stderr @@ -1,5 +1,5 @@ error: GraphQL input object expected all fields to have unique names - --> fail/input-object/derive_unique_name.rs:4:15 + --> fail/input_object/struct/derive_unique_name.rs:4:15 | 4 | struct Object { | _______________^ 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..6002b24ab --- /dev/null +++ b/tests/integration/tests/codegen_input_object_derive_enum.rs @@ -0,0 +1,1002 @@ +//! 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_multiple() { + // 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 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 + 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 { + 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 nested { + use super::*; + + #[derive(GraphQLInputObject)] + enum By { + User(UserBy), + } + + #[derive(GraphQLInputObject)] + enum UserBy { + Id(ID), + Username(prelude::String), + RegistrationNumber(i32), + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn user_info(by: By) -> prelude::String { + let By::User(by) = by; + 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: {user: {id: "123"}}) + userName: userInfo(by: {user: {username: "John"}}) + userNum: userInfo(by: {user: {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_multiple() { + // language=GraphQL + const DOC: &str = r#"{ + userInfo(by: {user: {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: \ + Error on \"By\" field \"user\": \ + Exactly one key must be specified", + &[SourcePosition::new(27, 1, 25)], + ) + .into()), + ); + } + + #[tokio::test] + async fn errs_on_no_fields() { + // language=GraphQL + const DOC: &str = r#"{ + userInfo(by: {user: {}}) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Err(RuleError::new( + "Invalid value for argument \"by\", reason: \ + Error on \"By\" field \"user\": \ + 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: {user: {id: null}}) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Err(RuleError::new( + "Invalid value for argument \"by\", reason: \ + Error on \"By\" field \"user\": \ + Value for member field \"id\" must be specified", + &[SourcePosition::new(27, 1, 25)], + ) + .into()), + ); + } +} + +mod ignored_variant { + use super::*; + + #[expect(dead_code, reason = "GraphQL schema testing")] + #[derive(GraphQLInputObject)] + enum UserBy { + Id(ID), + #[graphql(ignore)] + Username(prelude::String), + #[graphql(skip)] + RegistrationNumber(i32), + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn user_info(by: UserBy) -> prelude::String { + match by { + UserBy::Id(id) => id.into(), + UserBy::Username(_) => unreachable!(), + UserBy::RegistrationNumber(_) => unreachable!(), + } + } + } + + #[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 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 description_from_doc_comment { + use super::*; + + /// Selector for searching users. + #[derive(GraphQLInputObject)] + enum UserBy { + /// By ID selector. + Id(ID), + + /// By username selector. + Username(prelude::String), + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn user_info(by: UserBy) -> prelude::String { + match by { + UserBy::Id(id) => id.into(), + UserBy::Username(name) => name, + } + } + } + + #[tokio::test] + async fn resolves() { + // language=GraphQL + const DOC: &str = r#"{ + userId: userInfo(by: {id: "123"}) + userName: userInfo(by: {username: "John"}) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"userId": "123", "userName": "John"}), + vec![], + )), + ); + } + + #[tokio::test] + async fn has_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": "Selector for searching users.", + }}), + vec![], + )), + ); + } + + #[tokio::test] + async fn has_input_fields_descriptions() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "UserBy") { + inputFields { + name + description + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [{ + "name": "id", + "description": "By ID selector.", + }, { + "name": "username", + "description": "By username selector.", + }]}}), + vec![], + )), + ); + } +} + +mod description_and_name_from_graphql_attr { + use super::*; + + /// Ignored doc. + #[derive(GraphQLInputObject)] + #[graphql(name = "UserBy", desc = "Selector for searching users.")] + enum By { + /// Ignored doc. + #[graphql(name = "ID", description = "By ID selector.")] + Id(ID), + + /// By username selector. + Username(prelude::String), + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn user_info(by: By) -> prelude::String { + match by { + By::Id(id) => id.into(), + By::Username(name) => name, + } + } + } + + #[tokio::test] + async fn resolves() { + // language=GraphQL + const DOC: &str = r#"{ + userId: userInfo(by: {ID: "123"}) + userName: userInfo(by: {username: "John"}) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"userId": "123", "userName": "John"}), + vec![], + )), + ); + } + + #[tokio::test] + async fn has_description_and_name() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "UserBy") { + name + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": { + "name": "UserBy", + "description": "Selector for searching users.", + }}), + vec![], + )), + ); + } + + #[tokio::test] + async fn has_input_fields_descriptions_and_names() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "UserBy") { + inputFields { + name + description + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [{ + "name": "ID", + "description": "By ID selector.", + }, { + "name": "username", + "description": "By username selector.", + }]}}), + vec![], + )), + ); + } +} + +mod deprecation_from_graphql_attr { + use super::*; + + #[derive(GraphQLInputObject)] + enum UserBy { + Id(ID), + #[graphql(deprecated = "Do not use.")] + #[deprecated(note = "Should be omitted.")] + Username(prelude::String), + #[graphql(deprecated)] + #[deprecated(note = "Should be omitted.")] + RegistrationNumber(i32), + } + + struct QueryRoot; + + #[expect(deprecated, reason = "GraphQL schema testing")] + #[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 deprecates_fields() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "UserBy") { + inputFields(includeDeprecated: true) { + name + isDeprecated + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [ + {"name": "id", "isDeprecated": false}, + {"name": "username", "isDeprecated": true}, + {"name": "registrationNumber", "isDeprecated": true}, + ]}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn provides_deprecation_reason() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "UserBy") { + inputFields(includeDeprecated: true) { + name + deprecationReason + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [ + {"name": "id", "deprecationReason": null}, + {"name": "username", "deprecationReason": "Do not use."}, + {"name": "registrationNumber", "deprecationReason": null}, + ]}}), + vec![], + )), + ); + } +} + +mod deprecation_from_rust_attr { + use super::*; + + #[derive(GraphQLInputObject)] + enum UserBy { + Id(ID), + #[deprecated(note = "Should be omitted.")] + Username(prelude::String), + #[deprecated] + RegistrationNumber(i32), + } + + struct QueryRoot; + + #[expect(deprecated, reason = "GraphQL schema testing")] + #[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 deprecates_fields() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "UserBy") { + inputFields(includeDeprecated: true) { + name + isDeprecated + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [ + {"name": "id", "isDeprecated": false}, + {"name": "username", "isDeprecated": true}, + {"name": "registrationNumber", "isDeprecated": true}, + ]}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn provides_deprecation_reason() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "UserBy") { + inputFields(includeDeprecated: true) { + name + deprecationReason + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [ + {"name": "id", "deprecationReason": null}, + {"name": "username", "deprecationReason": "Should be omitted."}, + {"name": "registrationNumber", "deprecationReason": null}, + ]}}), + vec![], + )), + ); + } +} + +mod renamed_all_variants { + use super::*; + + #[derive(GraphQLInputObject)] + #[graphql(rename_all = "none")] + 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 has_input_fields() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "UserBy") { + inputFields { + name + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [ + {"name": "Id"}, + {"name": "Username"}, + {"name": "RegistrationNumber"}, + ]}}), + vec![], + )), + ); + } +} diff --git a/tests/integration/tests/codegen_input_object_derive.rs b/tests/integration/tests/codegen_input_object_derive_struct.rs similarity index 66% rename from tests/integration/tests/codegen_input_object_derive.rs rename to tests/integration/tests/codegen_input_object_derive_struct.rs index 4aa9e00bb..eedf282f6 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![], )), ); @@ -195,7 +196,8 @@ mod default_value { assert_eq!( execute(DOC, None, &schema, &graphql_vars! {}, &()).await, Err(RuleError::new( - "Invalid value for argument \"point\", reason: Error on \"Point2D\" field \"y\": \ + "Invalid value for argument \"point\", reason: \ + Error on \"Point2D\" field \"y\": \ \"null\" specified for not nullable type \"Float!\"", &[SourcePosition::new(11, 0, 11)], ) @@ -220,23 +222,6 @@ mod default_value { ); } - #[tokio::test] - async fn is_graphql_input_object() { - // language=GraphQL - const DOC: &str = r#"{ - __type(name: "Point2D") { - kind - } - }"#; - - let schema = schema(QueryRoot); - - assert_eq!( - execute(DOC, None, &schema, &graphql_vars! {}, &()).await, - Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])), - ); - } - #[tokio::test] async fn has_input_fields() { // language=GraphQL @@ -356,23 +341,6 @@ mod default_nullable_value { ); } - #[tokio::test] - async fn is_graphql_input_object() { - // language=GraphQL - const DOC: &str = r#"{ - __type(name: "Point2D") { - kind - } - }"#; - - let schema = schema(QueryRoot); - - assert_eq!( - execute(DOC, None, &schema, &graphql_vars! {}, &()).await, - Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])), - ); - } - #[tokio::test] async fn has_input_fields() { // language=GraphQL @@ -397,20 +365,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![], )), ); @@ -461,57 +426,6 @@ mod ignored_field { ); } - #[tokio::test] - async fn is_graphql_input_object() { - // language=GraphQL - const DOC: &str = r#"{ - __type(name: "Point2D") { - kind - } - }"#; - - let schema = schema(QueryRoot); - - assert_eq!( - execute(DOC, None, &schema, &graphql_vars! {}, &()).await, - Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), 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 @@ -520,12 +434,6 @@ mod ignored_field { inputFields { name description - type { - ofType { - name - } - } - defaultValue } } }"#; @@ -535,20 +443,13 @@ 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, + }, { + "name": "y", + "description": null, + }]}}), vec![], )), ); @@ -592,40 +493,6 @@ mod description_from_doc_comment { ); } - #[tokio::test] - async fn is_graphql_input_object() { - // language=GraphQL - const DOC: &str = r#"{ - __type(name: "Point2D") { - kind - } - }"#; - - let schema = schema(QueryRoot); - - assert_eq!( - execute(DOC, None, &schema, &graphql_vars! {}, &()).await, - Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), 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 @@ -649,19 +516,13 @@ mod description_from_doc_comment { } #[tokio::test] - async fn has_input_fields() { + async fn has_input_fields_descriptions() { // language=GraphQL const DOC: &str = r#"{ __type(name: "Point2D") { inputFields { name description - type { - ofType { - name - } - } - defaultValue } } }"#; @@ -671,27 +532,20 @@ 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.", + }, { + "name": "yCoord", + "description": "Ordinate value.", + }]}}), vec![], )), ); } } -mod description_from_graphql_attr { +mod description_and_name_from_graphql_attr { use super::*; /// Ignored doc. @@ -731,44 +585,11 @@ mod description_from_graphql_attr { } #[tokio::test] - async fn is_graphql_input_object() { - // language=GraphQL - const DOC: &str = r#"{ - __type(name: "Point") { - kind - } - }"#; - - let schema = schema(QueryRoot); - - assert_eq!( - execute(DOC, None, &schema, &graphql_vars! {}, &()).await, - Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])), - ); - } - - #[tokio::test] - async fn uses_type_name() { + async fn has_description_and_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 } }"#; @@ -779,27 +600,22 @@ mod description_from_graphql_attr { execute(DOC, None, &schema, &graphql_vars! {}, &()).await, Ok(( graphql_value!({"__type": { + "name": "Point", "description": "Point in a Cartesian system.", }}), - vec![] + vec![], )), ); } #[tokio::test] - async fn has_input_fields() { + async fn has_input_fields_descriptions_and_names() { // language=GraphQL const DOC: &str = r#"{ __type(name: "Point") { inputFields { name description - type { - ofType { - name - } - } - defaultValue } } }"#; @@ -809,20 +625,13 @@ 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.", + }, { + "name": "y", + "description": "Ordinate value.", + }]}}), vec![], )), ); @@ -868,59 +677,6 @@ mod deprecation_from_graphql_attr { ); } - #[tokio::test] - async fn is_graphql_input_object() { - // language=GraphQL - const DOC: &str = r#"{ - __type(name: "Point") { - kind - } - }"#; - - let schema = schema(QueryRoot); - - assert_eq!( - execute(DOC, None, &schema, &graphql_vars! {}, &()).await, - Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), 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 @@ -1017,59 +773,6 @@ mod deprecation_from_rust_attr { ); } - #[tokio::test] - async fn is_graphql_input_object() { - // language=GraphQL - const DOC: &str = r#"{ - __type(name: "Point") { - kind - } - }"#; - - let schema = schema(QueryRoot); - - assert_eq!( - execute(DOC, None, &schema, &graphql_vars! {}, &()).await, - Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), 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 @@ -1161,23 +864,6 @@ mod renamed_all_fields { ); } - #[tokio::test] - async fn is_graphql_input_object() { - // language=GraphQL - const DOC: &str = r#"{ - __type(name: "Point2D") { - kind - } - }"#; - - let schema = schema(QueryRoot); - - assert_eq!( - execute(DOC, None, &schema, &graphql_vars! {}, &()).await, - Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])), - ); - } - #[tokio::test] async fn has_input_fields() { // language=GraphQL @@ -1185,13 +871,6 @@ mod renamed_all_fields { __type(name: "Point2D") { inputFields { name - description - type { - ofType { - name - } - } - defaultValue } } }"#; @@ -1202,18 +881,8 @@ mod renamed_all_fields { 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, - }, + {"name": "x_coord"}, + {"name": "y"}, ]}}), vec![], )),