diff --git a/docs/book/content/advanced/implicit_and_explicit_null.md b/docs/book/content/advanced/implicit_and_explicit_null.md index 014249a19..746cb7ee3 100644 --- a/docs/book/content/advanced/implicit_and_explicit_null.md +++ b/docs/book/content/advanced/implicit_and_explicit_null.md @@ -99,10 +99,11 @@ impl Into for UserPatchInput { struct Context { session: Session, } +impl juniper::Context for Context {} struct Mutation; -#[juniper::graphql_object(Context=Context)] +#[juniper::graphql_object(context = Context)] impl Mutation { fn patch_user(ctx: &Context, patch: UserPatchInput) -> FieldResult { ctx.session.patch_user(patch.into())?; diff --git a/docs/book/content/advanced/subscriptions.md b/docs/book/content/advanced/subscriptions.md index 95b26513a..05e01d64b 100644 --- a/docs/book/content/advanced/subscriptions.md +++ b/docs/book/content/advanced/subscriptions.md @@ -25,10 +25,6 @@ This example shows a subscription operation that returns two events, the strings sequentially: ```rust -# extern crate futures; -# extern crate juniper; -# extern crate juniper_subscriptions; -# extern crate tokio; # use juniper::{graphql_object, graphql_subscription, FieldError}; # use futures::Stream; # use std::pin::Pin; @@ -40,7 +36,7 @@ sequentially: # pub struct Query; # #[graphql_object(context = Database)] # impl Query { -# fn hello_world() -> &str { +# fn hello_world() -> &'static str { # "Hello World!" # } # } @@ -110,7 +106,7 @@ where [`Connection`][Connection] is a `Stream` of values returned by the operati # # #[graphql_object(context = Database)] # impl Query { -# fn hello_world() -> &str { +# fn hello_world() -> &'static str { # "Hello World!" # } # } diff --git a/docs/book/content/quickstart.md b/docs/book/content/quickstart.md index 7af0bb02e..1fbcd882a 100644 --- a/docs/book/content/quickstart.md +++ b/docs/book/content/quickstart.md @@ -6,11 +6,9 @@ Juniper follows a [code-first approach][schema_approach] to defining GraphQL sch ## Installation -!FILENAME Cargo.toml - ```toml [dependencies] -juniper = { git = "https://github.com/graphql-rust/juniper" } +juniper = "0.15" ``` ## Schema example @@ -89,7 +87,7 @@ struct Query; context = Context, )] impl Query { - fn apiVersion() -> &str { + fn apiVersion() -> &'static str { "1.0" } @@ -114,14 +112,13 @@ struct Mutation; #[graphql_object( context = Context, - // If we need to use `ScalarValue` parametrization explicitly somewhere - // in the object definition (like here in `FieldResult`), we should + // in the object definition (like here in `FieldResult`), we could // declare an explicit type parameter for that, and specify it. - scalar = S, + scalar = S: ScalarValue + Display, )] -impl Mutation { - fn createHuman(context: &Context, new_human: NewHuman) -> FieldResult { +impl Mutation { + fn createHuman(context: &Context, new_human: NewHuman) -> FieldResult { let db = context.pool.get_connection().map_err(|e| e.map_scalar_value())?; let human: Human = db.insert_human(&new_human).map_err(|e| e.map_scalar_value())?; Ok(human) diff --git a/docs/book/content/types/interfaces.md b/docs/book/content/types/interfaces.md index b5e81e5ca..414d2f299 100644 --- a/docs/book/content/types/interfaces.md +++ b/docs/book/content/types/interfaces.md @@ -232,6 +232,21 @@ trait Character { # fn main() {} ``` +Renaming policies for all [GraphQL interface][1] fields and arguments are supported as well: +```rust +# #![allow(deprecated)] +# extern crate juniper; +use juniper::graphql_interface; + +#[graphql_interface(rename_all = "none")] // disables any renaming +trait Character { + // Now exposed as `my_id` and `my_num` in the schema + fn my_id(&self, my_num: i32) -> &str; +} +# +# fn main() {} +``` + ### Custom context diff --git a/docs/book/content/types/objects/complex_fields.md b/docs/book/content/types/objects/complex_fields.md index 75867184c..86d310eca 100644 --- a/docs/book/content/types/objects/complex_fields.md +++ b/docs/book/content/types/objects/complex_fields.md @@ -3,9 +3,10 @@ If you've got a struct that can't be mapped directly to GraphQL, that contains computed fields or circular structures, you have to use a more powerful tool: the `#[graphql_object]` procedural macro. This macro lets you define GraphQL object -fields in a Rust `impl` block for a type. Note that only GraphQL fields -can be specified in this `impl` block. If you want to define normal methods on the struct, -you have to do so in a separate, normal `impl` block. Continuing with the +fields in a Rust `impl` block for a type. Note, that GraphQL fields are defined in +this `impl` block by default. If you want to define normal methods on the struct, +you have to do so either in a separate "normal" `impl` block, or mark them with +`#[graphql(ignore)]` attribute to be omitted by the macro. Continuing with the example from the last chapter, this is how you would define `Person` using the macro: @@ -28,12 +29,15 @@ impl Person { fn age(&self) -> i32 { self.age } + + #[graphql(ignore)] + pub fn hidden_from_graphql(&self) { + // [...] + } } -// Note that this syntax generates an implementation of the GraphQLType trait, -// the base impl of your struct can still be written like usual: impl Person { - pub fn hidden_from_graphql(&self) { + pub fn hidden_from_graphql2(&self) { // [...] } } @@ -44,7 +48,6 @@ impl Person { While this is a bit more verbose, it lets you write any kind of function in the field resolver. With this syntax, fields can also take arguments: - ```rust # extern crate juniper; # use juniper::{graphql_object, GraphQLObject}; @@ -61,7 +64,7 @@ struct House { #[graphql_object] impl House { - // Creates the field inhabitantWithName(name), returning a nullable person + // Creates the field `inhabitantWithName(name)`, returning a nullable `Person`. fn inhabitant_with_name(&self, name: String) -> Option<&Person> { self.inhabitants.iter().find(|p| p.name == name) } @@ -127,15 +130,29 @@ impl Person { # fn main() { } ``` +Or provide a different renaming policy on a `impl` block for all its fields: +```rust +# extern crate juniper; +# use juniper::graphql_object; +struct Person; + +#[graphql_object(rename_all = "none")] // disables any renaming +impl Person { + // Now exposed as `renamed_field` in the schema + fn renamed_field() -> bool { + true + } +} +# +# fn main() {} +``` + ## Customizing arguments Method field arguments can also be customized. They can have custom descriptions and default values. -**Note**: The syntax for this is currently a little awkward. -This will become better once the [Rust RFC 2565](https://github.com/rust-lang/rust/issues/60406) is implemented. - ```rust # extern crate juniper; # use juniper::graphql_object; @@ -144,21 +161,22 @@ struct Person {} #[graphql_object] impl Person { - #[graphql( - arguments( - arg1( - // Set a default value which will be injected if not present. - // The default can be any valid Rust expression, including a function call, etc. - default = true, - // Set a description. - description = "The first argument..." - ), - arg2( - default = 0, - ) - ) - )] - fn field1(&self, arg1: bool, arg2: i32) -> String { + fn field1( + &self, + #[graphql( + // Arguments can also be renamed if required. + name = "arg", + // Set a default value which will be injected if not present. + // The default can be any valid Rust expression, including a function call, etc. + default = true, + // Set a description. + description = "The first argument..." + )] + arg1: bool, + // If default expression is not specified then `Default::default()` value is used. + #[graphql(default)] + arg2: i32, + ) -> String { format!("{} {}", arg1, arg2) } } @@ -166,13 +184,23 @@ impl Person { # fn main() { } ``` -## More features +Provide a different renaming policy on a `impl` block also implies for arguments: +```rust +# extern crate juniper; +# use juniper::graphql_object; +struct Person; -GraphQL fields expose more features than Rust's standard method syntax gives us: +#[graphql_object(rename_all = "none")] // disables any renaming +impl Person { + // Now exposed as `my_arg` in the schema + fn field(my_arg: bool) -> bool { + my_arg + } +} +# +# fn main() {} +``` -* Per-field description and deprecation messages -* Per-argument default values -* Per-argument descriptions +## More features -These, and more features, are described more thoroughly in [the reference -documentation](https://docs.rs/juniper/latest/juniper/macro.object.html). +These, and more features, are described more thoroughly in [the reference documentation](https://docs.rs/juniper/latest/juniper/attr.graphql_object.html). diff --git a/docs/book/content/types/objects/defining_objects.md b/docs/book/content/types/objects/defining_objects.md index 1a4dcb7da..5f32c6862 100644 --- a/docs/book/content/types/objects/defining_objects.md +++ b/docs/book/content/types/objects/defining_objects.md @@ -152,7 +152,22 @@ struct Person { name: String, age: i32, #[graphql(name = "websiteURL")] - website_url: Option, // Now exposed as websiteURL in the schema + website_url: Option, // now exposed as `websiteURL` in the schema +} +# +# fn main() {} +``` + +Or provide a different renaming policy on a struct for all its fields: +```rust +# extern crate juniper; +# use juniper::GraphQLObject; +#[derive(GraphQLObject)] +#[graphql(rename_all = "none")] // disables any renaming +struct Person { + name: String, + age: i32, + website_url: Option, // now exposed as `website_url` in the schema } # # fn main() {} @@ -181,9 +196,9 @@ The `name`, `description`, and `deprecation` arguments can of course be combined. Some restrictions from the GraphQL spec still applies though; you can only deprecate object fields and enum values. -## Skipping fields +## Ignoring fields -By default all fields in a `GraphQLObject` are included in the generated GraphQL type. To prevent including a specific field, annotate the field with `#[graphql(skip)]`: +By default, all fields in a `GraphQLObject` are included in the generated GraphQL type. To prevent including a specific field, annotate the field with `#[graphql(ignore)]`: ```rust # extern crate juniper; @@ -192,9 +207,9 @@ By default all fields in a `GraphQLObject` are included in the generated GraphQL struct Person { name: String, age: i32, - #[graphql(skip)] + #[graphql(ignore)] # #[allow(dead_code)] - password_hash: String, // This cannot be queried or modified from GraphQL + password_hash: String, // cannot be queried or modified from GraphQL } # # fn main() {} diff --git a/docs/book/content/types/objects/error_handling.md b/docs/book/content/types/objects/error_handling.md index ea64e7642..4adc269fa 100644 --- a/docs/book/content/types/objects/error_handling.md +++ b/docs/book/content/types/objects/error_handling.md @@ -17,8 +17,7 @@ it will bubble up to the surrounding framework and hopefully be dealt with there. For recoverable errors, Juniper works well with the built-in `Result` type, you -can use the `?` operator or the `try!` macro and things will generally just work -as you expect them to: +can use the `?` operator and things will generally just work as you expect them to: ```rust # extern crate juniper; @@ -36,7 +35,7 @@ struct Example { #[graphql_object] impl Example { - fn contents() -> FieldResult { + fn contents(&self) -> FieldResult { let mut file = File::open(&self.filename)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; @@ -44,13 +43,10 @@ impl Example { } fn foo() -> FieldResult> { - // Some invalid bytes. - let invalid = vec![128, 223]; + // Some invalid bytes. + let invalid = vec![128, 223]; - match str::from_utf8(&invalid) { - Ok(s) => Ok(Some(s.to_string())), - Err(e) => Err(e)?, - } + Ok(Some(str::from_utf8(&invalid)?.to_string())) } } # @@ -66,7 +62,6 @@ there - those errors are automatically converted into `FieldError`. Juniper's error behavior conforms to the [GraphQL specification](https://spec.graphql.org/June2018/#sec-Errors-and-Non-Nullability). - When a field returns an error, the field's result is replaced by `null`, an additional `errors` object is created at the top level of the response, and the execution is resumed. For example, with the previous example and the following @@ -86,12 +81,12 @@ returned: !FILENAME Response for nullable field with error -```js +```json { "data": { "example": { contents: "", - foo: null, + foo: null } }, "errors": [ @@ -120,7 +115,7 @@ following would be returned: !FILENAME Response for non-null field with error and no nullable parent -```js +```json { "errors": [ "message": "Permission denied (os error 13)", @@ -162,11 +157,11 @@ struct Example { #[graphql_object] impl Example { - fn whatever() -> Result { - if let Some(value) = self.whatever { - return Ok(value); - } - Err(CustomError::WhateverNotSet) + fn whatever(&self) -> Result { + if let Some(value) = self.whatever { + return Ok(value); + } + Err(CustomError::WhateverNotSet) } } # diff --git a/docs/book/content/types/objects/using_contexts.md b/docs/book/content/types/objects/using_contexts.md index 0576a6644..10164e466 100644 --- a/docs/book/content/types/objects/using_contexts.md +++ b/docs/book/content/types/objects/using_contexts.md @@ -63,8 +63,8 @@ impl User { // with the context type. // Note: // - the type must be a reference - // - the name of the argument SHOULD be context - fn friends(&self, context: &Database) -> Vec<&User> { + // - the name of the argument SHOULD be `context` + fn friends<'db>(&self, context: &'db Database) -> Vec<&'db User> { // 5. Use the database to lookup users self.friend_ids.iter() diff --git a/examples/actix_subscriptions/src/main.rs b/examples/actix_subscriptions/src/main.rs index 6464a71eb..c5021ae6a 100644 --- a/examples/actix_subscriptions/src/main.rs +++ b/examples/actix_subscriptions/src/main.rs @@ -11,7 +11,7 @@ use actix_web::{ use juniper::{ graphql_object, graphql_subscription, tests::fixtures::starwars::schema::{Character as _, Database, Query}, - DefaultScalarValue, EmptyMutation, FieldError, RootNode, + DefaultScalarValue, EmptyMutation, FieldError, RootNode, Value, }; use juniper_actix::{graphql_handler, playground_handler, subscriptions::subscriptions_handler}; use juniper_graphql_ws::ConnectionConfig; diff --git a/examples/basic_subscriptions/Cargo.toml b/examples/basic_subscriptions/Cargo.toml index ccb294db8..8367cd167 100644 --- a/examples/basic_subscriptions/Cargo.toml +++ b/examples/basic_subscriptions/Cargo.toml @@ -5,8 +5,6 @@ edition = "2018" publish = false authors = ["Jordao Rosario "] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] futures = "0.3" serde = { version = "1.0", features = ["derive"] } diff --git a/examples/basic_subscriptions/src/main.rs b/examples/basic_subscriptions/src/main.rs index a8e3736c3..82d4b46fb 100644 --- a/examples/basic_subscriptions/src/main.rs +++ b/examples/basic_subscriptions/src/main.rs @@ -24,7 +24,7 @@ pub struct Query; #[graphql_object(context = Database)] impl Query { - fn hello_world() -> &str { + fn hello_world() -> &'static str { "Hello World!" } } diff --git a/examples/warp_subscriptions/Cargo.toml b/examples/warp_subscriptions/Cargo.toml index 5e0e76cfe..4c0b0025d 100644 --- a/examples/warp_subscriptions/Cargo.toml +++ b/examples/warp_subscriptions/Cargo.toml @@ -13,6 +13,7 @@ serde_json = "1.0" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } warp = "0.3" async-stream = "0.3" + juniper = { path = "../../juniper" } juniper_graphql_ws = { path = "../../juniper_graphql_ws" } juniper_warp = { path = "../../juniper_warp", features = ["subscriptions"] } diff --git a/examples/warp_subscriptions/src/main.rs b/examples/warp_subscriptions/src/main.rs index 21e2668bb..c7a6f855a 100644 --- a/examples/warp_subscriptions/src/main.rs +++ b/examples/warp_subscriptions/src/main.rs @@ -5,7 +5,7 @@ use std::{env, pin::Pin, sync::Arc, time::Duration}; use futures::{FutureExt as _, Stream}; use juniper::{ graphql_object, graphql_subscription, DefaultScalarValue, EmptyMutation, FieldError, - GraphQLEnum, RootNode, + GraphQLEnum, RootNode, Value, }; use juniper_graphql_ws::ConnectionConfig; use juniper_warp::{playground_filter, subscriptions::serve_graphql_ws}; diff --git a/integration_tests/codegen_fail/fail/interface/implementer_non_object_type.stderr b/integration_tests/codegen_fail/fail/interface/implementer_non_object_type.stderr index e4e09b66a..597171164 100644 --- a/integration_tests/codegen_fail/fail/interface/implementer_non_object_type.stderr +++ b/integration_tests/codegen_fail/fail/interface/implementer_non_object_type.stderr @@ -1,10 +1,10 @@ -error[E0277]: the trait bound `ObjA: GraphQLObjectType<__S>` is not satisfied +error[E0277]: the trait bound `ObjA: GraphQLObject<__S>` is not satisfied --> $DIR/implementer_non_object_type.rs:15:1 | 15 | #[graphql_interface(for = ObjA)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `GraphQLObjectType<__S>` is not implemented for `ObjA` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `GraphQLObject<__S>` is not implemented for `ObjA` | - = note: required by `juniper::marker::GraphQLObjectType::mark` + = note: required by `juniper::GraphQLObject::mark` = note: this error originates in the attribute macro `graphql_interface` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `ObjA: IsOutputType<__S>` is not satisfied diff --git a/integration_tests/codegen_fail/fail/object/argument_double_underscored.rs b/integration_tests/codegen_fail/fail/object/argument_double_underscored.rs new file mode 100644 index 000000000..b85c97216 --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/argument_double_underscored.rs @@ -0,0 +1,12 @@ +use juniper::graphql_object; + +struct Obj; + +#[graphql_object] +impl Obj { + fn id(&self, __num: i32) -> &str { + "funA" + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/impl_no_argument_underscore.stderr b/integration_tests/codegen_fail/fail/object/argument_double_underscored.stderr similarity index 64% rename from integration_tests/codegen_fail/fail/object/impl_no_argument_underscore.stderr rename to integration_tests/codegen_fail/fail/object/argument_double_underscored.stderr index 40eea80dc..433c024e0 100644 --- a/integration_tests/codegen_fail/fail/object/impl_no_argument_underscore.stderr +++ b/integration_tests/codegen_fail/fail/object/argument_double_underscored.stderr @@ -1,7 +1,7 @@ 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. - --> $DIR/impl_no_argument_underscore.rs:5:29 + --> $DIR/argument_double_underscored.rs:7:18 | -5 | #[graphql(arguments(arg(name = "__arg")))] - | ^^^^ +7 | fn id(&self, __num: i32) -> &str { + | ^^^^^ | = note: https://spec.graphql.org/June2018/#sec-Schema diff --git a/integration_tests/codegen_fail/fail/object/argument_non_input_type.rs b/integration_tests/codegen_fail/fail/object/argument_non_input_type.rs new file mode 100644 index 000000000..73dce0de0 --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/argument_non_input_type.rs @@ -0,0 +1,17 @@ +use juniper::{graphql_object, GraphQLObject}; + +#[derive(GraphQLObject)] +struct ObjA { + test: String, +} + +struct ObjB; + +#[graphql_object] +impl ObjB { + fn id(&self, obj: ObjA) -> &str { + "funA" + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/argument_non_input_type.stderr b/integration_tests/codegen_fail/fail/object/argument_non_input_type.stderr new file mode 100644 index 000000000..1a054e5ef --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/argument_non_input_type.stderr @@ -0,0 +1,16 @@ +error[E0277]: the trait bound `ObjA: IsInputType<__S>` is not satisfied + --> $DIR/argument_non_input_type.rs:10:1 + | +10 | #[graphql_object] + | ^^^^^^^^^^^^^^^^^ the trait `IsInputType<__S>` is not implemented for `ObjA` + | + = note: required by `juniper::marker::IsInputType::mark` + = note: this error originates in the attribute macro `graphql_object` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `ObjA: FromInputValue<__S>` is not satisfied + --> $DIR/argument_non_input_type.rs:10:1 + | +10 | #[graphql_object] + | ^^^^^^^^^^^^^^^^^ the trait `FromInputValue<__S>` is not implemented for `ObjA` + | + = note: this error originates in the attribute macro `graphql_object` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/object/argument_wrong_default_array.rs b/integration_tests/codegen_fail/fail/object/argument_wrong_default_array.rs new file mode 100644 index 000000000..fab39ef73 --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/argument_wrong_default_array.rs @@ -0,0 +1,12 @@ +use juniper::graphql_object; + +struct ObjA; + +#[graphql_object] +impl ObjA { + fn wrong(&self, #[graphql(default = [true, false, false])] input: [bool; 2]) -> bool { + input[0] + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/argument_wrong_default_array.stderr b/integration_tests/codegen_fail/fail/object/argument_wrong_default_array.stderr new file mode 100644 index 000000000..83128b5c8 --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/argument_wrong_default_array.stderr @@ -0,0 +1,12 @@ +error[E0277]: the trait bound `[bool; 2]: From<[bool; 3]>` is not satisfied + --> $DIR/argument_wrong_default_array.rs:5:1 + | +5 | #[graphql_object] + | ^^^^^^^^^^^^^^^^^ the trait `From<[bool; 3]>` is not implemented for `[bool; 2]` + | + = help: the following implementations were found: + <&'a [ascii::ascii_char::AsciiChar] as From<&'a ascii::ascii_str::AsciiStr>> + <&'a [u8] as From<&'a ascii::ascii_str::AsciiStr>> + <&'a mut [ascii::ascii_char::AsciiChar] as From<&'a mut ascii::ascii_str::AsciiStr>> + = note: required because of the requirements on the impl of `Into<[bool; 2]>` for `[bool; 3]` + = note: this error originates in the attribute macro `graphql_object` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/object/attr_field_double_underscored.rs b/integration_tests/codegen_fail/fail/object/attr_field_double_underscored.rs new file mode 100644 index 000000000..ff854d0a7 --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/attr_field_double_underscored.rs @@ -0,0 +1,12 @@ +use juniper::graphql_object; + +struct ObjA; + +#[graphql_object] +impl Character for ObjA { + fn __id(&self) -> &str { + "funA" + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/attr_field_double_underscored.stderr b/integration_tests/codegen_fail/fail/object/attr_field_double_underscored.stderr new file mode 100644 index 000000000..cd1849d85 --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/attr_field_double_underscored.stderr @@ -0,0 +1,7 @@ +error: #[graphql_object] attribute is applicable to non-trait `impl` blocks only + --> $DIR/attr_field_double_underscored.rs:5:1 + | +5 | #[graphql_object] + | ^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `graphql_object` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/object/attr_field_non_output_return_type.rs b/integration_tests/codegen_fail/fail/object/attr_field_non_output_return_type.rs new file mode 100644 index 000000000..c5d748b51 --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/attr_field_non_output_return_type.rs @@ -0,0 +1,17 @@ +use juniper::{graphql_object, GraphQLInputObject}; + +#[derive(GraphQLInputObject)] +struct ObjB { + id: i32, +} + +struct ObjA; + +#[graphql_object] +impl ObjA { + fn id(&self) -> ObjB { + ObjB { id: 34 } + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/attr_field_non_output_return_type.stderr b/integration_tests/codegen_fail/fail/object/attr_field_non_output_return_type.stderr new file mode 100644 index 000000000..9bc4c6bda --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/attr_field_non_output_return_type.stderr @@ -0,0 +1,8 @@ +error[E0277]: the trait bound `ObjB: IsOutputType<__S>` is not satisfied + --> $DIR/attr_field_non_output_return_type.rs:10:1 + | +10 | #[graphql_object] + | ^^^^^^^^^^^^^^^^^ the trait `IsOutputType<__S>` is not implemented for `ObjB` + | + = note: required by `juniper::marker::IsOutputType::mark` + = note: this error originates in the attribute macro `graphql_object` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/object/attr_fields_duplicate.rs b/integration_tests/codegen_fail/fail/object/attr_fields_duplicate.rs new file mode 100644 index 000000000..4fe14664c --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/attr_fields_duplicate.rs @@ -0,0 +1,17 @@ +use juniper::graphql_object; + +struct ObjA; + +#[graphql_object] +impl ObjA { + fn id(&self) -> &str { + "funA" + } + + #[graphql(name = "id")] + fn id2(&self) -> &str { + "funB" + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/attr_fields_duplicate.stderr b/integration_tests/codegen_fail/fail/object/attr_fields_duplicate.stderr new file mode 100644 index 000000000..468e4cde5 --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/attr_fields_duplicate.stderr @@ -0,0 +1,7 @@ +error: GraphQL object must have a different name for each field + --> $DIR/attr_fields_duplicate.rs:6:6 + | +6 | impl ObjA { + | ^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Objects diff --git a/integration_tests/codegen_fail/fail/object/attr_name_double_underscored.rs b/integration_tests/codegen_fail/fail/object/attr_name_double_underscored.rs new file mode 100644 index 000000000..37ec8ba70 --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/attr_name_double_underscored.rs @@ -0,0 +1,12 @@ +use juniper::graphql_object; + +struct __Obj; + +#[graphql_object] +impl __Obj { + fn id(&self) -> &str { + "funA" + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/impl_no_underscore.stderr b/integration_tests/codegen_fail/fail/object/attr_name_double_underscored.stderr similarity index 72% rename from integration_tests/codegen_fail/fail/object/impl_no_underscore.stderr rename to integration_tests/codegen_fail/fail/object/attr_name_double_underscored.stderr index b7e531e5c..e7bedddbf 100644 --- a/integration_tests/codegen_fail/fail/object/impl_no_underscore.stderr +++ b/integration_tests/codegen_fail/fail/object/attr_name_double_underscored.stderr @@ -1,7 +1,7 @@ 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. - --> $DIR/impl_no_underscore.rs:5:15 + --> $DIR/attr_name_double_underscored.rs:6:6 | -5 | #[graphql(name = "__test")] - | ^^^^ +6 | impl __Obj { + | ^^^^^ | = note: https://spec.graphql.org/June2018/#sec-Schema diff --git a/integration_tests/codegen_fail/fail/object/attr_no_fields.rs b/integration_tests/codegen_fail/fail/object/attr_no_fields.rs new file mode 100644 index 000000000..b36c58697 --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/attr_no_fields.rs @@ -0,0 +1,8 @@ +use juniper::graphql_object; + +struct Obj; + +#[graphql_object] +impl Obj {} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/attr_no_fields.stderr b/integration_tests/codegen_fail/fail/object/attr_no_fields.stderr new file mode 100644 index 000000000..f783cc91a --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/attr_no_fields.stderr @@ -0,0 +1,7 @@ +error: GraphQL object must have at least one field + --> $DIR/attr_no_fields.rs:6:6 + | +6 | impl Obj {} + | ^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Objects diff --git a/integration_tests/codegen_fail/fail/object/attr_wrong_item.rs b/integration_tests/codegen_fail/fail/object/attr_wrong_item.rs new file mode 100644 index 000000000..92ba57ec2 --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/attr_wrong_item.rs @@ -0,0 +1,6 @@ +use juniper::graphql_object; + +#[graphql_object] +enum Character {} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/attr_wrong_item.stderr b/integration_tests/codegen_fail/fail/object/attr_wrong_item.stderr new file mode 100644 index 000000000..cf9141485 --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/attr_wrong_item.stderr @@ -0,0 +1,7 @@ +error: #[graphql_object] attribute is applicable to non-trait `impl` blocks only + --> $DIR/attr_wrong_item.rs:3:1 + | +3 | #[graphql_object] + | ^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `graphql_object` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/object/derive_field_double_underscored.rs b/integration_tests/codegen_fail/fail/object/derive_field_double_underscored.rs new file mode 100644 index 000000000..286383edf --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/derive_field_double_underscored.rs @@ -0,0 +1,8 @@ +use juniper::GraphQLObject; + +#[derive(GraphQLObject)] +struct Object { + __test: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/derive_no_underscore.stderr b/integration_tests/codegen_fail/fail/object/derive_field_double_underscored.stderr similarity index 71% rename from integration_tests/codegen_fail/fail/object/derive_no_underscore.stderr rename to integration_tests/codegen_fail/fail/object/derive_field_double_underscored.stderr index eb7eb67b5..a6f3cb8db 100644 --- a/integration_tests/codegen_fail/fail/object/derive_no_underscore.stderr +++ b/integration_tests/codegen_fail/fail/object/derive_field_double_underscored.stderr @@ -1,7 +1,7 @@ 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. - --> $DIR/derive_no_underscore.rs:3:15 + --> $DIR/derive_field_double_underscored.rs:5:5 | -3 | #[graphql(name = "__test")] - | ^^^^ +5 | __test: String, + | ^^^^^^ | = note: https://spec.graphql.org/June2018/#sec-Schema diff --git a/integration_tests/codegen_fail/fail/object/derive_field_non_output_return_type.rs b/integration_tests/codegen_fail/fail/object/derive_field_non_output_return_type.rs new file mode 100644 index 000000000..7197cd595 --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/derive_field_non_output_return_type.rs @@ -0,0 +1,13 @@ +use juniper::{GraphQLInputObject, GraphQLObject}; + +#[derive(GraphQLInputObject)] +struct ObjB { + id: i32, +} + +#[derive(GraphQLObject)] +struct ObjA { + id: ObjB, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/derive_field_non_output_return_type.stderr b/integration_tests/codegen_fail/fail/object/derive_field_non_output_return_type.stderr new file mode 100644 index 000000000..d112debd2 --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/derive_field_non_output_return_type.stderr @@ -0,0 +1,8 @@ +error[E0277]: the trait bound `ObjB: IsOutputType<__S>` is not satisfied + --> $DIR/derive_field_non_output_return_type.rs:8:10 + | +8 | #[derive(GraphQLObject)] + | ^^^^^^^^^^^^^ the trait `IsOutputType<__S>` is not implemented for `ObjB` + | + = note: required by `juniper::marker::IsOutputType::mark` + = note: this error originates in the derive macro `GraphQLObject` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/object/derive_fields_duplicate.rs b/integration_tests/codegen_fail/fail/object/derive_fields_duplicate.rs new file mode 100644 index 000000000..cd73e1f3d --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/derive_fields_duplicate.rs @@ -0,0 +1,10 @@ +use juniper::GraphQLObject; + +#[derive(GraphQLObject)] +struct ObjA { + id: String, + #[graphql(name = "id")] + id2: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/derive_fields_duplicate.stderr b/integration_tests/codegen_fail/fail/object/derive_fields_duplicate.stderr new file mode 100644 index 000000000..74004561b --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/derive_fields_duplicate.stderr @@ -0,0 +1,11 @@ +error: GraphQL object must have a different name for each field + --> $DIR/derive_fields_duplicate.rs:4:1 + | +4 | / struct ObjA { +5 | | id: String, +6 | | #[graphql(name = "id")] +7 | | id2: String, +8 | | } + | |_^ + | + = note: https://spec.graphql.org/June2018/#sec-Objects diff --git a/integration_tests/codegen_fail/fail/object/derive_fields_unique.rs b/integration_tests/codegen_fail/fail/object/derive_fields_unique.rs deleted file mode 100644 index ee5afa112..000000000 --- a/integration_tests/codegen_fail/fail/object/derive_fields_unique.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[derive(juniper::GraphQLObject)] -struct Object { - test: String, - #[graphql(name = "test")] - test2: String, -} - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/derive_fields_unique.stderr b/integration_tests/codegen_fail/fail/object/derive_fields_unique.stderr deleted file mode 100644 index 999b01168..000000000 --- a/integration_tests/codegen_fail/fail/object/derive_fields_unique.stderr +++ /dev/null @@ -1,9 +0,0 @@ -error: GraphQL object does not allow fields with the same name - --> $DIR/derive_fields_unique.rs:4:5 - | -4 | / #[graphql(name = "test")] -5 | | test2: String, - | |_________________^ - | - = help: There is at least one other field with the same name `test`, possibly renamed via the #[graphql] attribute - = note: https://spec.graphql.org/June2018/#sec-Objects diff --git a/integration_tests/codegen_fail/fail/object/derive_incompatible_input_object.rs.disabled b/integration_tests/codegen_fail/fail/object/derive_incompatible_input_object.rs.disabled deleted file mode 100644 index 0e0122190..000000000 --- a/integration_tests/codegen_fail/fail/object/derive_incompatible_input_object.rs.disabled +++ /dev/null @@ -1,12 +0,0 @@ -// FIXME: enable this if interfaces are supported -#[derive(juniper::GraphQLInputObject)] -struct ObjectA { - test: String, -} - -#[derive(juniper::GraphQLObject)] -struct Object { - field: ObjectA, -} - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/derive_name_double_underscored.rs b/integration_tests/codegen_fail/fail/object/derive_name_double_underscored.rs new file mode 100644 index 000000000..209b3f37b --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/derive_name_double_underscored.rs @@ -0,0 +1,8 @@ +use juniper::GraphQLObject; + +#[derive(GraphQLObject)] +struct __Obj { + id: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/derive_name_double_underscored.stderr b/integration_tests/codegen_fail/fail/object/derive_name_double_underscored.stderr new file mode 100644 index 000000000..11eafcef6 --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/derive_name_double_underscored.stderr @@ -0,0 +1,7 @@ +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. + --> $DIR/derive_name_double_underscored.rs:4:8 + | +4 | struct __Obj { + | ^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Schema diff --git a/integration_tests/codegen_fail/fail/object/derive_no_fields.rs b/integration_tests/codegen_fail/fail/object/derive_no_fields.rs index cd72632ae..27795d55a 100644 --- a/integration_tests/codegen_fail/fail/object/derive_no_fields.rs +++ b/integration_tests/codegen_fail/fail/object/derive_no_fields.rs @@ -1,4 +1,6 @@ -#[derive(juniper::GraphQLObject)] -struct Object {} +use juniper::GraphQLObject; + +#[derive(GraphQLObject)] +struct Obj {} fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/derive_no_fields.stderr b/integration_tests/codegen_fail/fail/object/derive_no_fields.stderr index 183cd4304..1975c3611 100644 --- a/integration_tests/codegen_fail/fail/object/derive_no_fields.stderr +++ b/integration_tests/codegen_fail/fail/object/derive_no_fields.stderr @@ -1,7 +1,7 @@ -error: GraphQL object expects at least one field - --> $DIR/derive_no_fields.rs:2:1 +error: GraphQL object must have at least one field + --> $DIR/derive_no_fields.rs:4:1 | -2 | struct Object {} - | ^^^^^^^^^^^^^^^^ +4 | struct Obj {} + | ^^^^^^^^^^^^^ | = note: https://spec.graphql.org/June2018/#sec-Objects diff --git a/integration_tests/codegen_fail/fail/object/derive_no_underscore.rs b/integration_tests/codegen_fail/fail/object/derive_no_underscore.rs deleted file mode 100644 index 9b270b546..000000000 --- a/integration_tests/codegen_fail/fail/object/derive_no_underscore.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[derive(juniper::GraphQLObject)] -struct Object { - #[graphql(name = "__test")] - test: String, -} - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/derive_wrong_item.rs b/integration_tests/codegen_fail/fail/object/derive_wrong_item.rs new file mode 100644 index 000000000..8318256fc --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/derive_wrong_item.rs @@ -0,0 +1,6 @@ +use juniper::GraphQLObject; + +#[derive(GraphQLObject)] +enum Character {} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/derive_wrong_item.stderr b/integration_tests/codegen_fail/fail/object/derive_wrong_item.stderr new file mode 100644 index 000000000..50680bec9 --- /dev/null +++ b/integration_tests/codegen_fail/fail/object/derive_wrong_item.stderr @@ -0,0 +1,5 @@ +error: GraphQL object can only be derived for structs + --> $DIR/derive_wrong_item.rs:4:1 + | +4 | enum Character {} + | ^^^^^^^^^^^^^^^^^ diff --git a/integration_tests/codegen_fail/fail/object/impl_argument_no_object.rs b/integration_tests/codegen_fail/fail/object/impl_argument_no_object.rs deleted file mode 100644 index a584a16f3..000000000 --- a/integration_tests/codegen_fail/fail/object/impl_argument_no_object.rs +++ /dev/null @@ -1,15 +0,0 @@ -#[derive(juniper::GraphQLObject)] -struct Obj { - field: String, -} - -struct Object {} - -#[juniper::graphql_object] -impl Object { - fn test(&self, test: Obj) -> String { - String::new() - } -} - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/impl_argument_no_object.stderr b/integration_tests/codegen_fail/fail/object/impl_argument_no_object.stderr deleted file mode 100644 index 384686082..000000000 --- a/integration_tests/codegen_fail/fail/object/impl_argument_no_object.stderr +++ /dev/null @@ -1,29 +0,0 @@ -error[E0277]: the trait bound `Obj: IsInputType<__S>` is not satisfied - --> $DIR/impl_argument_no_object.rs:8:1 - | -8 | #[juniper::graphql_object] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IsInputType<__S>` is not implemented for `Obj` - | - = note: required by `juniper::marker::IsInputType::mark` - = note: this error originates in the attribute macro `juniper::graphql_object` (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0277]: the trait bound `Obj: FromInputValue<__S>` is not satisfied - --> $DIR/impl_argument_no_object.rs:8:1 - | -8 | #[juniper::graphql_object] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `FromInputValue<__S>` is not implemented for `Obj` - | - = note: this error originates in the attribute macro `juniper::graphql_object` (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0277]: the trait bound `Obj: FromInputValue` is not satisfied - --> $DIR/impl_argument_no_object.rs:8:1 - | -8 | #[juniper::graphql_object] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `FromInputValue` is not implemented for `Obj` - | - ::: $WORKSPACE/juniper/src/ast.rs - | - | pub trait FromInputValue: Sized { - | ------------------------------------------------------- required by this bound in `FromInputValue` - | - = note: this error originates in the attribute macro `juniper::graphql_object` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/object/impl_argument_wrong_default_array.rs b/integration_tests/codegen_fail/fail/object/impl_argument_wrong_default_array.rs deleted file mode 100644 index 740400c4d..000000000 --- a/integration_tests/codegen_fail/fail/object/impl_argument_wrong_default_array.rs +++ /dev/null @@ -1,11 +0,0 @@ -struct Object; - -#[juniper::graphql_object] -impl Object { - #[graphql(arguments(input(default = [true, false, false])))] - fn wrong(input: [bool; 2]) -> bool { - input[0] - } -} - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/impl_argument_wrong_default_array.stderr b/integration_tests/codegen_fail/fail/object/impl_argument_wrong_default_array.stderr deleted file mode 100644 index eb85ff256..000000000 --- a/integration_tests/codegen_fail/fail/object/impl_argument_wrong_default_array.stderr +++ /dev/null @@ -1,7 +0,0 @@ -error[E0308]: mismatched types - --> $DIR/impl_argument_wrong_default_array.rs:3:1 - | -3 | #[juniper::graphql_object] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ expected an array with a fixed size of 2 elements, found one with 3 elements - | - = note: this error originates in the attribute macro `juniper::graphql_object` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/object/impl_fields_unique.rs b/integration_tests/codegen_fail/fail/object/impl_fields_unique.rs deleted file mode 100644 index c89fc9aa7..000000000 --- a/integration_tests/codegen_fail/fail/object/impl_fields_unique.rs +++ /dev/null @@ -1,14 +0,0 @@ -struct Object {} - -#[juniper::graphql_object] -impl Object { - fn test(&self) -> String { - String::new() - } - - fn test(&self) -> String { - String::new() - } -} - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/impl_fields_unique.stderr b/integration_tests/codegen_fail/fail/object/impl_fields_unique.stderr deleted file mode 100644 index 49649a2ac..000000000 --- a/integration_tests/codegen_fail/fail/object/impl_fields_unique.stderr +++ /dev/null @@ -1,10 +0,0 @@ -error: GraphQL object does not allow fields with the same name - --> $DIR/impl_fields_unique.rs:9:5 - | -9 | / fn test(&self) -> String { -10 | | String::new() -11 | | } - | |_____^ - | - = help: There is at least one other field with the same name `test`, possibly renamed via the #[graphql] attribute - = note: https://spec.graphql.org/June2018/#sec-Objects diff --git a/integration_tests/codegen_fail/fail/object/impl_incompatible_input_object.rs.disabled b/integration_tests/codegen_fail/fail/object/impl_incompatible_input_object.rs.disabled deleted file mode 100644 index e7661e402..000000000 --- a/integration_tests/codegen_fail/fail/object/impl_incompatible_input_object.rs.disabled +++ /dev/null @@ -1,19 +0,0 @@ -// FIXME: enable this if interfaces are supported -#[derive(juniper::GraphQLInputObject)] -#[graphql(scalar = juniper::DefaultScalarValue)] -struct Obj { - field: String, -} - -struct Object {} - -#[juniper::graphql_object] -impl Object { - fn test(&self) -> Obj { - Obj { - field: String::new(), - } - } -} - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/impl_no_argument_underscore.rs b/integration_tests/codegen_fail/fail/object/impl_no_argument_underscore.rs deleted file mode 100644 index 02fec848e..000000000 --- a/integration_tests/codegen_fail/fail/object/impl_no_argument_underscore.rs +++ /dev/null @@ -1,11 +0,0 @@ -struct Object {} - -#[juniper::graphql_object] -impl Object { - #[graphql(arguments(arg(name = "__arg")))] - fn test(&self, arg: String) -> String { - arg - } -} - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/impl_no_fields.rs b/integration_tests/codegen_fail/fail/object/impl_no_fields.rs deleted file mode 100644 index aa863c7cd..000000000 --- a/integration_tests/codegen_fail/fail/object/impl_no_fields.rs +++ /dev/null @@ -1,6 +0,0 @@ -struct Object {} - -#[juniper::graphql_object] -impl Object {} - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/object/impl_no_fields.stderr b/integration_tests/codegen_fail/fail/object/impl_no_fields.stderr deleted file mode 100644 index 798a7db20..000000000 --- a/integration_tests/codegen_fail/fail/object/impl_no_fields.stderr +++ /dev/null @@ -1,7 +0,0 @@ -error: GraphQL object expects at least one field - --> $DIR/impl_no_fields.rs:4:1 - | -4 | impl Object {} - | ^^^^^^^^^^^^^^ - | - = note: https://spec.graphql.org/June2018/#sec-Objects diff --git a/integration_tests/codegen_fail/fail/object/impl_no_underscore.rs b/integration_tests/codegen_fail/fail/object/impl_no_underscore.rs deleted file mode 100644 index 5e9ddbfac..000000000 --- a/integration_tests/codegen_fail/fail/object/impl_no_underscore.rs +++ /dev/null @@ -1,11 +0,0 @@ -struct Object {} - -#[juniper::graphql_object] -impl Object { - #[graphql(name = "__test")] - fn test(&self) -> String { - String::new() - } -} - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/subscription/argument_double_underscored.rs b/integration_tests/codegen_fail/fail/subscription/argument_double_underscored.rs new file mode 100644 index 000000000..fe01fc69e --- /dev/null +++ b/integration_tests/codegen_fail/fail/subscription/argument_double_underscored.rs @@ -0,0 +1,16 @@ +use std::pin::Pin; + +use juniper::graphql_subscription; + +type Stream<'a, I> = Pin + Send + 'a>>; + +struct Obj; + +#[graphql_subscription] +impl Obj { + async fn id(&self, __num: i32) -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("funA"))) + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/subscription/argument_double_underscored.stderr b/integration_tests/codegen_fail/fail/subscription/argument_double_underscored.stderr new file mode 100644 index 000000000..97d9d6df1 --- /dev/null +++ b/integration_tests/codegen_fail/fail/subscription/argument_double_underscored.stderr @@ -0,0 +1,7 @@ +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. + --> $DIR/argument_double_underscored.rs:11:24 + | +11 | async fn id(&self, __num: i32) -> Stream<'static, &'static str> { + | ^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Schema diff --git a/integration_tests/codegen_fail/fail/subscription/argument_non_input_type.rs b/integration_tests/codegen_fail/fail/subscription/argument_non_input_type.rs new file mode 100644 index 000000000..90b24bb21 --- /dev/null +++ b/integration_tests/codegen_fail/fail/subscription/argument_non_input_type.rs @@ -0,0 +1,22 @@ +use std::pin::Pin; + +use futures::{future, stream}; +use juniper::{graphql_subscription, GraphQLObject}; + +type Stream<'a, I> = Pin + Send + 'a>>; + +#[derive(GraphQLObject)] +struct ObjA { + test: String, +} + +struct ObjB; + +#[graphql_subscription] +impl ObjB { + async fn id(&self, obj: ObjA) -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("funA"))) + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/subscription/argument_non_input_type.stderr b/integration_tests/codegen_fail/fail/subscription/argument_non_input_type.stderr new file mode 100644 index 000000000..87d4a9ee7 --- /dev/null +++ b/integration_tests/codegen_fail/fail/subscription/argument_non_input_type.stderr @@ -0,0 +1,16 @@ +error[E0277]: the trait bound `ObjA: IsInputType<__S>` is not satisfied + --> $DIR/argument_non_input_type.rs:15:1 + | +15 | #[graphql_subscription] + | ^^^^^^^^^^^^^^^^^^^^^^^ the trait `IsInputType<__S>` is not implemented for `ObjA` + | + = note: required by `juniper::marker::IsInputType::mark` + = note: this error originates in the attribute macro `graphql_subscription` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `ObjA: FromInputValue<__S>` is not satisfied + --> $DIR/argument_non_input_type.rs:15:1 + | +15 | #[graphql_subscription] + | ^^^^^^^^^^^^^^^^^^^^^^^ the trait `FromInputValue<__S>` is not implemented for `ObjA` + | + = note: this error originates in the attribute macro `graphql_subscription` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/subscription/argument_wrong_default_array.rs b/integration_tests/codegen_fail/fail/subscription/argument_wrong_default_array.rs new file mode 100644 index 000000000..c0ec68639 --- /dev/null +++ b/integration_tests/codegen_fail/fail/subscription/argument_wrong_default_array.rs @@ -0,0 +1,20 @@ +use std::pin::Pin; + +use futures::{future, stream}; +use juniper::graphql_subscription; + +type Stream<'a, I> = Pin + Send + 'a>>; + +struct ObjA; + +#[graphql_subscription] +impl ObjA { + async fn wrong( + &self, + #[graphql(default = [true, false, false])] input: [bool; 2], + ) -> Stream<'static, bool> { + Box::pin(stream::once(future::ready(input[0]))) + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/subscription/argument_wrong_default_array.stderr b/integration_tests/codegen_fail/fail/subscription/argument_wrong_default_array.stderr new file mode 100644 index 000000000..56b033cbb --- /dev/null +++ b/integration_tests/codegen_fail/fail/subscription/argument_wrong_default_array.stderr @@ -0,0 +1,12 @@ +error[E0277]: the trait bound `[bool; 2]: From<[bool; 3]>` is not satisfied + --> $DIR/argument_wrong_default_array.rs:10:1 + | +10 | #[graphql_subscription] + | ^^^^^^^^^^^^^^^^^^^^^^^ the trait `From<[bool; 3]>` is not implemented for `[bool; 2]` + | + = help: the following implementations were found: + <&'a [ascii::ascii_char::AsciiChar] as From<&'a ascii::ascii_str::AsciiStr>> + <&'a [u8] as From<&'a ascii::ascii_str::AsciiStr>> + <&'a mut [ascii::ascii_char::AsciiChar] as From<&'a mut ascii::ascii_str::AsciiStr>> + = note: required because of the requirements on the impl of `Into<[bool; 2]>` for `[bool; 3]` + = note: this error originates in the attribute macro `graphql_subscription` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/subscription/field_double_underscored.rs b/integration_tests/codegen_fail/fail/subscription/field_double_underscored.rs new file mode 100644 index 000000000..686dccc27 --- /dev/null +++ b/integration_tests/codegen_fail/fail/subscription/field_double_underscored.rs @@ -0,0 +1,16 @@ +use std::pin::Pin; + +use juniper::graphql_subscription; + +type Stream<'a, I> = Pin + Send + 'a>>; + +struct ObjA; + +#[graphql_subscription] +impl Character for ObjA { + async fn __id() -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("funA"))) + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/subscription/field_double_underscored.stderr b/integration_tests/codegen_fail/fail/subscription/field_double_underscored.stderr new file mode 100644 index 000000000..1f5e519af --- /dev/null +++ b/integration_tests/codegen_fail/fail/subscription/field_double_underscored.stderr @@ -0,0 +1,7 @@ +error: #[graphql_subscription] attribute is applicable to non-trait `impl` blocks only + --> $DIR/field_double_underscored.rs:9:1 + | +9 | #[graphql_subscription] + | ^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `graphql_subscription` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/subscription/field_non_output_return_type.rs b/integration_tests/codegen_fail/fail/subscription/field_non_output_return_type.rs new file mode 100644 index 000000000..8cc918542 --- /dev/null +++ b/integration_tests/codegen_fail/fail/subscription/field_non_output_return_type.rs @@ -0,0 +1,22 @@ +use std::pin::Pin; + +use futures::{future, stream}; +use juniper::{graphql_subscription, GraphQLInputObject}; + +type Stream<'a, I> = Pin + Send + 'a>>; + +#[derive(GraphQLInputObject)] +struct ObjB { + id: i32, +} + +struct ObjA; + +#[graphql_subscription] +impl ObjA { + async fn id(&self) -> Stream<'static, ObjB> { + Box::pin(stream::once(future::ready(ObjB { id: 34 }))) + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/subscription/field_non_output_return_type.stderr b/integration_tests/codegen_fail/fail/subscription/field_non_output_return_type.stderr new file mode 100644 index 000000000..84d4cf63b --- /dev/null +++ b/integration_tests/codegen_fail/fail/subscription/field_non_output_return_type.stderr @@ -0,0 +1,8 @@ +error[E0277]: the trait bound `ObjB: IsOutputType<__S>` is not satisfied + --> $DIR/field_non_output_return_type.rs:15:1 + | +15 | #[graphql_subscription] + | ^^^^^^^^^^^^^^^^^^^^^^^ the trait `IsOutputType<__S>` is not implemented for `ObjB` + | + = note: required by `juniper::marker::IsOutputType::mark` + = note: this error originates in the attribute macro `graphql_subscription` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/subscription/field_not_async.rs b/integration_tests/codegen_fail/fail/subscription/field_not_async.rs new file mode 100644 index 000000000..f9cfb5b53 --- /dev/null +++ b/integration_tests/codegen_fail/fail/subscription/field_not_async.rs @@ -0,0 +1,16 @@ +use std::pin::Pin; + +use juniper::graphql_subscription; + +type Stream<'a, I> = Pin + Send + 'a>>; + +struct ObjA; + +#[graphql_subscription] +impl ObjA { + fn id(&self) -> Stream<'static, bool> { + Box::pin(stream::once(future::ready(true))) + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/subscription/field_not_async.stderr b/integration_tests/codegen_fail/fail/subscription/field_not_async.stderr new file mode 100644 index 000000000..3e5209c8e --- /dev/null +++ b/integration_tests/codegen_fail/fail/subscription/field_not_async.stderr @@ -0,0 +1,8 @@ +error: GraphQL object synchronous resolvers are not supported + --> $DIR/field_not_async.rs:11:5 + | +11 | fn id(&self) -> Stream<'static, bool> { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Objects + = note: Specify that this function is async: `async fn foo()` diff --git a/integration_tests/codegen_fail/fail/subscription/fields_duplicate.rs b/integration_tests/codegen_fail/fail/subscription/fields_duplicate.rs new file mode 100644 index 000000000..22ccc9778 --- /dev/null +++ b/integration_tests/codegen_fail/fail/subscription/fields_duplicate.rs @@ -0,0 +1,21 @@ +use std::pin::Pin; + +use juniper::graphql_subscription; + +type Stream<'a, I> = Pin + Send + 'a>>; + +struct ObjA; + +#[graphql_subscription] +impl ObjA { + async fn id(&self) -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("funA"))) + } + + #[graphql(name = "id")] + async fn id2(&self) -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("funB"))) + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/subscription/fields_duplicate.stderr b/integration_tests/codegen_fail/fail/subscription/fields_duplicate.stderr new file mode 100644 index 000000000..d9156450c --- /dev/null +++ b/integration_tests/codegen_fail/fail/subscription/fields_duplicate.stderr @@ -0,0 +1,7 @@ +error: GraphQL object must have a different name for each field + --> $DIR/fields_duplicate.rs:10:6 + | +10 | impl ObjA { + | ^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Objects diff --git a/integration_tests/codegen_fail/fail/subscription/name_double_underscored.rs b/integration_tests/codegen_fail/fail/subscription/name_double_underscored.rs new file mode 100644 index 000000000..d9911148b --- /dev/null +++ b/integration_tests/codegen_fail/fail/subscription/name_double_underscored.rs @@ -0,0 +1,16 @@ +use std::pin::Pin; + +use juniper::graphql_subscription; + +type Stream<'a, I> = Pin + Send + 'a>>; + +struct __Obj; + +#[graphql_subscription] +impl __Obj { + fn id(&self) -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("funA"))) + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/subscription/name_double_underscored.stderr b/integration_tests/codegen_fail/fail/subscription/name_double_underscored.stderr new file mode 100644 index 000000000..ac1081e35 --- /dev/null +++ b/integration_tests/codegen_fail/fail/subscription/name_double_underscored.stderr @@ -0,0 +1,7 @@ +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. + --> $DIR/name_double_underscored.rs:10:6 + | +10 | impl __Obj { + | ^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Schema diff --git a/integration_tests/codegen_fail/fail/subscription/no_fields.rs b/integration_tests/codegen_fail/fail/subscription/no_fields.rs new file mode 100644 index 000000000..0c199c13c --- /dev/null +++ b/integration_tests/codegen_fail/fail/subscription/no_fields.rs @@ -0,0 +1,8 @@ +use juniper::graphql_subscription; + +struct Obj; + +#[graphql_subscription] +impl Obj {} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/subscription/no_fields.stderr b/integration_tests/codegen_fail/fail/subscription/no_fields.stderr new file mode 100644 index 000000000..93b5487f5 --- /dev/null +++ b/integration_tests/codegen_fail/fail/subscription/no_fields.stderr @@ -0,0 +1,7 @@ +error: GraphQL object must have at least one field + --> $DIR/no_fields.rs:6:6 + | +6 | impl Obj {} + | ^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Objects diff --git a/integration_tests/codegen_fail/fail/subscription/wrong_item.rs b/integration_tests/codegen_fail/fail/subscription/wrong_item.rs new file mode 100644 index 000000000..3ab174c6f --- /dev/null +++ b/integration_tests/codegen_fail/fail/subscription/wrong_item.rs @@ -0,0 +1,6 @@ +use juniper::graphql_subscription; + +#[graphql_subscription] +enum Character {} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/subscription/wrong_item.stderr b/integration_tests/codegen_fail/fail/subscription/wrong_item.stderr new file mode 100644 index 000000000..d149b56be --- /dev/null +++ b/integration_tests/codegen_fail/fail/subscription/wrong_item.stderr @@ -0,0 +1,7 @@ +error: #[graphql_subscription] attribute is applicable to non-trait `impl` blocks only + --> $DIR/wrong_item.rs:3:1 + | +3 | #[graphql_subscription] + | ^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `graphql_subscription` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/union/enum_non_object_variant.stderr b/integration_tests/codegen_fail/fail/union/enum_non_object_variant.stderr index 3cb23c268..fc7f13fa1 100644 --- a/integration_tests/codegen_fail/fail/union/enum_non_object_variant.stderr +++ b/integration_tests/codegen_fail/fail/union/enum_non_object_variant.stderr @@ -1,8 +1,8 @@ -error[E0277]: the trait bound `Test: GraphQLObjectType<__S>` is not satisfied +error[E0277]: the trait bound `Test: GraphQLObject<__S>` is not satisfied --> $DIR/enum_non_object_variant.rs:9:10 | 9 | #[derive(GraphQLUnion)] - | ^^^^^^^^^^^^ the trait `GraphQLObjectType<__S>` is not implemented for `Test` + | ^^^^^^^^^^^^ the trait `GraphQLObject<__S>` is not implemented for `Test` | - = note: required by `juniper::marker::GraphQLObjectType::mark` + = note: required by `juniper::GraphQLObject::mark` = note: this error originates in the derive macro `GraphQLUnion` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/union/struct_non_object_variant.stderr b/integration_tests/codegen_fail/fail/union/struct_non_object_variant.stderr index 8981bacfc..200aeecdf 100644 --- a/integration_tests/codegen_fail/fail/union/struct_non_object_variant.stderr +++ b/integration_tests/codegen_fail/fail/union/struct_non_object_variant.stderr @@ -1,8 +1,8 @@ -error[E0277]: the trait bound `Test: GraphQLObjectType<__S>` is not satisfied +error[E0277]: the trait bound `Test: GraphQLObject<__S>` is not satisfied --> $DIR/struct_non_object_variant.rs:9:10 | 9 | #[derive(GraphQLUnion)] - | ^^^^^^^^^^^^ the trait `GraphQLObjectType<__S>` is not implemented for `Test` + | ^^^^^^^^^^^^ the trait `GraphQLObject<__S>` is not implemented for `Test` | - = note: required by `juniper::marker::GraphQLObjectType::mark` + = note: required by `juniper::GraphQLObject::mark` = note: this error originates in the derive macro `GraphQLUnion` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/union/trait_non_object_variant.stderr b/integration_tests/codegen_fail/fail/union/trait_non_object_variant.stderr index 95cb302b5..e9ac7cdb7 100644 --- a/integration_tests/codegen_fail/fail/union/trait_non_object_variant.stderr +++ b/integration_tests/codegen_fail/fail/union/trait_non_object_variant.stderr @@ -1,8 +1,8 @@ -error[E0277]: the trait bound `Test: GraphQLObjectType<__S>` is not satisfied +error[E0277]: the trait bound `Test: GraphQLObject<__S>` is not satisfied --> $DIR/trait_non_object_variant.rs:9:1 | 9 | #[graphql_union] - | ^^^^^^^^^^^^^^^^ the trait `GraphQLObjectType<__S>` is not implemented for `Test` + | ^^^^^^^^^^^^^^^^ the trait `GraphQLObject<__S>` is not implemented for `Test` | - = note: required by `juniper::marker::GraphQLObjectType::mark` + = note: required by `juniper::GraphQLObject::mark` = note: this error originates in the attribute macro `graphql_union` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/juniper_tests/src/array.rs b/integration_tests/juniper_tests/src/array.rs index 43cbd8ea4..aa1606511 100644 --- a/integration_tests/juniper_tests/src/array.rs +++ b/integration_tests/juniper_tests/src/array.rs @@ -145,8 +145,7 @@ mod as_input_argument { input[0] } - #[graphql(arguments(input(default = [true, false, false])))] - fn third(input: [bool; 3]) -> bool { + fn third(#[graphql(default = [true, false, false])] input: [bool; 3]) -> bool { input[2] } } diff --git a/integration_tests/juniper_tests/src/codegen/derive_enum.rs b/integration_tests/juniper_tests/src/codegen/derive_enum.rs index 47329b9b0..26b0f2e1f 100644 --- a/integration_tests/juniper_tests/src/codegen/derive_enum.rs +++ b/integration_tests/juniper_tests/src/codegen/derive_enum.rs @@ -69,7 +69,7 @@ fn test_derived_enum() { let meta = SomeEnum::meta(&(), &mut registry); assert_eq!(meta.name(), Some("Some")); - assert_eq!(meta.description(), Some(&"enum descr".to_string())); + assert_eq!(meta.description(), Some("enum descr")); // Test no rename variant. assert_eq!( @@ -102,24 +102,21 @@ fn test_derived_enum() { fn test_doc_comment() { let mut registry: Registry = Registry::new(FnvHashMap::default()); let meta = DocEnum::meta(&(), &mut registry); - assert_eq!(meta.description(), Some(&"Enum doc.".to_string())); + assert_eq!(meta.description(), Some("Enum doc.")); } #[test] fn test_multi_doc_comment() { let mut registry: Registry = Registry::new(FnvHashMap::default()); let meta = MultiDocEnum::meta(&(), &mut registry); - assert_eq!( - meta.description(), - Some(&"Doc 1. Doc 2.\n\nDoc 4.".to_string()) - ); + assert_eq!(meta.description(), Some("Doc 1. Doc 2.\n\nDoc 4.")); } #[test] fn test_doc_comment_override() { let mut registry: Registry = Registry::new(FnvHashMap::default()); let meta = OverrideDocEnum::meta(&(), &mut registry); - assert_eq!(meta.description(), Some(&"enum override".to_string())); + assert_eq!(meta.description(), Some("enum override")); } fn test_context(_t: T) diff --git a/integration_tests/juniper_tests/src/codegen/derive_input_object.rs b/integration_tests/juniper_tests/src/codegen/derive_input_object.rs index cc1595f5e..9b4c72909 100644 --- a/integration_tests/juniper_tests/src/codegen/derive_input_object.rs +++ b/integration_tests/juniper_tests/src/codegen/derive_input_object.rs @@ -115,7 +115,7 @@ fn test_derived_input_object() { let mut registry: Registry = Registry::new(FnvHashMap::default()); let meta = Input::meta(&(), &mut registry); assert_eq!(meta.name(), Some("MyInput")); - assert_eq!(meta.description(), Some(&"input descr".to_string())); + assert_eq!(meta.description(), Some("input descr")); // Test default value injection. @@ -173,22 +173,19 @@ fn test_derived_input_object() { fn test_doc_comment() { let mut registry: Registry = Registry::new(FnvHashMap::default()); let meta = DocComment::meta(&(), &mut registry); - assert_eq!(meta.description(), Some(&"Object comment.".to_string())); + assert_eq!(meta.description(), Some("Object comment.")); } #[test] fn test_multi_doc_comment() { let mut registry: Registry = Registry::new(FnvHashMap::default()); let meta = MultiDocComment::meta(&(), &mut registry); - assert_eq!( - meta.description(), - Some(&"Doc 1. Doc 2.\n\nDoc 4.".to_string()) - ); + assert_eq!(meta.description(), Some("Doc 1. Doc 2.\n\nDoc 4.")); } #[test] fn test_doc_comment_override() { let mut registry: Registry = Registry::new(FnvHashMap::default()); let meta = OverrideDocComment::meta(&(), &mut registry); - assert_eq!(meta.description(), Some(&"obj override".to_string())); + assert_eq!(meta.description(), Some("obj override")); } diff --git a/integration_tests/juniper_tests/src/codegen/derive_object.rs b/integration_tests/juniper_tests/src/codegen/derive_object.rs deleted file mode 100644 index 2a5336aba..000000000 --- a/integration_tests/juniper_tests/src/codegen/derive_object.rs +++ /dev/null @@ -1,502 +0,0 @@ -use fnv::FnvHashMap; -use juniper::{ - execute, graphql_object, DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLObject, - GraphQLType, Object, Registry, RootNode, Value, Variables, -}; - -#[derive(GraphQLObject, Debug, PartialEq)] -#[graphql(name = "MyObj", description = "obj descr")] -struct Obj { - regular_field: bool, - #[graphql( - name = "renamedField", - description = "descr", - deprecated = "field deprecation" - )] - c: i32, -} - -#[derive(GraphQLObject, Debug, PartialEq)] -struct Nested { - obj: Obj, -} - -/// Object comment. -#[derive(GraphQLObject, Debug, PartialEq)] -struct DocComment { - /// Field comment. - regular_field: bool, -} - -/// Doc 1.\ -/// Doc 2. -/// -/// Doc 4. -#[derive(GraphQLObject, Debug, PartialEq)] -struct MultiDocComment { - /// Field 1. - /// Field 2. - regular_field: bool, -} - -/// This is not used as the description. -#[derive(GraphQLObject, Debug, PartialEq)] -#[graphql(description = "obj override")] -struct OverrideDocComment { - /// This is not used as the description. - #[graphql(description = "field override")] - regular_field: bool, -} - -#[derive(GraphQLObject, Debug, PartialEq)] -struct WithLifetime<'a> { - regular_field: &'a i32, -} - -#[derive(GraphQLObject, Debug, PartialEq)] -struct SkippedFieldObj { - regular_field: bool, - #[graphql(skip)] - skipped: i32, -} - -#[derive(GraphQLObject, Debug, PartialEq)] -#[graphql(rename = "none")] -struct NoRenameObj { - one_field: bool, - another_field: i32, -} - -struct Context; -impl juniper::Context for Context {} - -#[derive(GraphQLObject, Debug)] -#[graphql(context = Context)] -struct WithCustomContext { - a: bool, -} - -struct Query; - -#[graphql_object] -impl Query { - fn obj() -> Obj { - Obj { - regular_field: true, - c: 22, - } - } - - fn nested() -> Nested { - Nested { - obj: Obj { - regular_field: false, - c: 333, - }, - } - } - - fn doc() -> DocComment { - DocComment { - regular_field: true, - } - } - - fn multi_doc() -> MultiDocComment { - MultiDocComment { - regular_field: true, - } - } - - fn override_doc() -> OverrideDocComment { - OverrideDocComment { - regular_field: true, - } - } - - fn skipped_field_obj() -> SkippedFieldObj { - SkippedFieldObj { - regular_field: false, - skipped: 42, - } - } - - fn no_rename_obj() -> NoRenameObj { - NoRenameObj { - one_field: true, - another_field: 146, - } - } -} - -struct NoRenameQuery; - -#[graphql_object(rename = "none")] -impl NoRenameQuery { - fn obj() -> Obj { - Obj { - regular_field: false, - c: 22, - } - } - - fn no_rename_obj() -> NoRenameObj { - NoRenameObj { - one_field: true, - another_field: 146, - } - } -} - -#[tokio::test] -async fn test_doc_comment_simple() { - let mut registry: Registry = Registry::new(FnvHashMap::default()); - let meta = DocComment::meta(&(), &mut registry); - assert_eq!(meta.description(), Some(&"Object comment.".to_string())); - - check_descriptions( - "DocComment", - &Value::scalar("Object comment."), - "regularField", - &Value::scalar("Field comment."), - ) - .await; -} - -#[tokio::test] -async fn test_multi_doc_comment() { - let mut registry: Registry = Registry::new(FnvHashMap::default()); - let meta = MultiDocComment::meta(&(), &mut registry); - assert_eq!( - meta.description(), - Some(&"Doc 1. Doc 2.\n\nDoc 4.".to_string()) - ); - - check_descriptions( - "MultiDocComment", - &Value::scalar("Doc 1. Doc 2.\n\nDoc 4."), - "regularField", - &Value::scalar("Field 1.\nField 2."), - ) - .await; -} - -#[tokio::test] -async fn test_doc_comment_override() { - let mut registry: Registry = Registry::new(FnvHashMap::default()); - let meta = OverrideDocComment::meta(&(), &mut registry); - assert_eq!(meta.description(), Some(&"obj override".to_string())); - - check_descriptions( - "OverrideDocComment", - &Value::scalar("obj override"), - "regularField", - &Value::scalar("field override"), - ) - .await; -} - -#[tokio::test] -async fn test_derived_object() { - assert_eq!( - >::name(&()), - Some("MyObj") - ); - - // Verify meta info. - let mut registry: Registry = Registry::new(FnvHashMap::default()); - let meta = Obj::meta(&(), &mut registry); - - assert_eq!(meta.name(), Some("MyObj")); - assert_eq!(meta.description(), Some(&"obj descr".to_string())); - - let doc = r#" - { - obj { - regularField - renamedField - } - }"#; - - let schema = RootNode::new( - Query, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); - - assert_eq!( - execute(doc, None, &schema, &Variables::new(), &()).await, - Ok(( - Value::object( - vec![( - "obj", - Value::object( - vec![ - ("regularField", Value::scalar(true)), - ("renamedField", Value::scalar(22)), - ] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect() - ), - vec![] - )) - ); -} - -#[tokio::test] -#[should_panic] -async fn test_cannot_query_skipped_field() { - let doc = r#" - { - skippedFieldObj { - skippedField - } - }"#; - let schema = RootNode::new( - Query, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); - execute(doc, None, &schema, &Variables::new(), &()) - .await - .unwrap(); -} - -#[tokio::test] -async fn test_skipped_field_siblings_unaffected() { - let doc = r#" - { - skippedFieldObj { - regularField - } - }"#; - let schema = RootNode::new( - Query, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); - execute(doc, None, &schema, &Variables::new(), &()) - .await - .unwrap(); -} - -#[tokio::test] -async fn test_derived_object_nested() { - let doc = r#" - { - nested { - obj { - regularField - renamedField - } - } - }"#; - - let schema = RootNode::new( - Query, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); - - assert_eq!( - execute(doc, None, &schema, &Variables::new(), &()).await, - Ok(( - Value::object( - vec![( - "nested", - Value::object( - vec![( - "obj", - Value::object( - vec![ - ("regularField", Value::scalar(false)), - ("renamedField", Value::scalar(333)), - ] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect() - ), - vec![] - )) - ); -} - -#[tokio::test] -async fn test_no_rename_root() { - let doc = r#" - { - no_rename_obj { - one_field - another_field - } - - obj { - regularField - } - }"#; - - let schema = RootNode::new( - NoRenameQuery, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); - - assert_eq!( - execute(doc, None, &schema, &Variables::new(), &()).await, - Ok(( - Value::object( - vec![ - ( - "no_rename_obj", - Value::object( - vec![ - ("one_field", Value::scalar(true)), - ("another_field", Value::scalar(146)), - ] - .into_iter() - .collect(), - ), - ), - ( - "obj", - Value::object( - vec![("regularField", Value::scalar(false)),] - .into_iter() - .collect(), - ), - ) - ] - .into_iter() - .collect() - ), - vec![] - )) - ); -} - -#[tokio::test] -async fn test_no_rename_obj() { - let doc = r#" - { - noRenameObj { - one_field - another_field - } - }"#; - - let schema = RootNode::new( - Query, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); - - assert_eq!( - execute(doc, None, &schema, &Variables::new(), &()).await, - Ok(( - Value::object( - vec![( - "noRenameObj", - Value::object( - vec![ - ("one_field", Value::scalar(true)), - ("another_field", Value::scalar(146)), - ] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect() - ), - vec![] - )) - ); -} - -async fn check_descriptions( - object_name: &str, - object_description: &Value, - field_name: &str, - field_value: &Value, -) { - let doc = format!( - r#" - {{ - __type(name: "{}") {{ - name, - description, - fields {{ - name - description - }} - }} - }} - "#, - object_name - ); - let _result = run_type_info_query(&doc, |(type_info, values)| { - assert_eq!( - type_info.get_field_value("name"), - Some(&Value::scalar(object_name)) - ); - assert_eq!( - type_info.get_field_value("description"), - Some(object_description) - ); - assert!(values.contains(&Value::object( - vec![ - ("name", Value::scalar(field_name)), - ("description", field_value.clone()), - ] - .into_iter() - .collect(), - ))); - }) - .await; -} - -async fn run_type_info_query(doc: &str, f: F) -where - F: Fn((&Object, &Vec)) -> (), -{ - let schema = RootNode::new( - Query, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); - - let (result, errs) = execute(doc, None, &schema, &Variables::new(), &()) - .await - .expect("Execution failed"); - - assert_eq!(errs, []); - - println!("Result: {:#?}", result); - - let type_info = result - .as_object_value() - .expect("Result is not an object") - .get_field_value("__type") - .expect("__type field missing") - .as_object_value() - .expect("__type field not an object value"); - - let fields = type_info - .get_field_value("fields") - .expect("fields field missing") - .as_list_value() - .expect("fields not a list"); - - f((type_info, fields)); -} diff --git a/integration_tests/juniper_tests/src/codegen/impl_object.rs b/integration_tests/juniper_tests/src/codegen/impl_object.rs deleted file mode 100644 index 7a7245ff6..000000000 --- a/integration_tests/juniper_tests/src/codegen/impl_object.rs +++ /dev/null @@ -1,139 +0,0 @@ -use juniper::{ - execute, graphql_object, DefaultScalarValue, EmptyMutation, EmptySubscription, Object, - RootNode, Value, Variables, -}; - -pub struct MyObject; - -#[graphql_object] -impl MyObject { - #[graphql(arguments(arg(name = "test")))] - fn test(&self, arg: String) -> String { - arg - } -} - -#[tokio::test] -async fn check_argument_rename() { - let doc = format!( - r#" - {{ - __type(name: "{}") {{ - name, - description, - fields {{ - name - description - }} - }} - }} - "#, - "MyObject" - ); - - run_type_info_query(&doc, |(_, values)| { - assert_eq!( - *values, - vec![Value::object( - vec![ - ("name", Value::scalar("test")), - ("description", Value::null()), - ] - .into_iter() - .collect(), - )] - ); - }) - .await; -} - -async fn run_type_info_query(doc: &str, f: F) -where - F: Fn((&Object, &Vec)) -> (), -{ - let schema = RootNode::new( - MyObject, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); - - let (result, errs) = execute(doc, None, &schema, &Variables::new(), &()) - .await - .expect("Execution failed"); - - assert_eq!(errs, []); - - println!("Result: {:#?}", result); - - let type_info = result - .as_object_value() - .expect("Result is not an object") - .get_field_value("__type") - .expect("__type field missing") - .as_object_value() - .expect("__type field not an object value"); - - let fields = type_info - .get_field_value("fields") - .expect("fields field missing") - .as_list_value() - .expect("fields not a list"); - - f((type_info, fields)); -} - -mod fallible { - use juniper::{graphql_object, FieldError}; - - struct Obj; - - #[graphql_object] - impl Obj { - fn test(&self, arg: String) -> Result { - Ok(arg) - } - } -} - -mod raw_argument { - use juniper::{ - graphql_object, graphql_value, EmptyMutation, EmptySubscription, RootNode, Variables, - }; - - struct Obj; - - #[graphql_object] - impl Obj { - #[graphql(arguments(r#arg(description = "The only argument")))] - fn test(&self, arg: String) -> String { - arg - } - } - - #[tokio::test] - async fn named_correctly() { - let doc = r#"{ - __type(name: "Obj") { - fields { - args { - name - } - } - } - }"#; - - let schema = RootNode::new( - Obj, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); - - assert_eq!( - juniper::execute(&doc, None, &schema, &Variables::new(), &()).await, - Ok(( - graphql_value!({"__type": {"fields": [{"args": [{"name": "arg"}]}]}}), - vec![], - )), - ); - } -} diff --git a/integration_tests/juniper_tests/src/codegen/interface_attr.rs b/integration_tests/juniper_tests/src/codegen/interface_attr.rs index b85f4f195..54d2e6e12 100644 --- a/integration_tests/juniper_tests/src/codegen/interface_attr.rs +++ b/integration_tests/juniper_tests/src/codegen/interface_attr.rs @@ -2,8 +2,8 @@ use juniper::{ execute, graphql_interface, graphql_object, graphql_value, DefaultScalarValue, EmptyMutation, - EmptySubscription, Executor, FieldError, FieldResult, GraphQLObject, GraphQLType, - IntoFieldError, RootNode, ScalarValue, Variables, + EmptySubscription, Executor, FieldError, FieldResult, GraphQLInputObject, GraphQLObject, + GraphQLType, IntoFieldError, RootNode, ScalarValue, Variables, }; fn schema<'q, C, Q>(query_root: Q) -> RootNode<'q, Q, EmptyMutation, EmptySubscription> @@ -46,13 +46,13 @@ mod no_implers { struct QueryRoot; - #[graphql_object] + #[graphql_object(scalar = S: ScalarValue + Send + Sync)] impl QueryRoot { fn character(&self) -> CharacterValue { unimplemented!() } - fn hero(&self) -> Box> { + fn hero(&self) -> Box> { unimplemented!() } } @@ -183,8 +183,8 @@ mod trivial { Droid, } - #[graphql_object(scalar = S)] - impl QueryRoot { + #[graphql_object(scalar = S: ScalarValue + Send + Sync)] + impl QueryRoot { fn character(&self) -> CharacterValue { match self { Self::Human => Human { @@ -200,7 +200,7 @@ mod trivial { } } - fn hero(&self) -> Box> { + fn hero(&self) -> Box> { let ch: Box> = match self { Self::Human => Box::new(Human { id: "human-32".to_string(), @@ -705,8 +705,8 @@ mod trivial_async { Droid, } - #[graphql_object(scalar = S)] - impl QueryRoot { + #[graphql_object(scalar = S: ScalarValue + Send + Sync)] + impl QueryRoot { fn character(&self) -> CharacterValue { match self { Self::Human => Human { @@ -722,7 +722,7 @@ mod trivial_async { } } - fn hero(&self) -> Box> { + fn hero(&self) -> Box> { let ch: Box> = match self { Self::Human => Box::new(Human { id: "human-32".to_string(), @@ -1068,8 +1068,8 @@ mod explicit_async { Droid, } - #[graphql_object(scalar = S)] - impl QueryRoot { + #[graphql_object(scalar = S: ScalarValue + Send + Sync)] + impl QueryRoot { fn character(&self) -> CharacterValue { match self { Self::Human => Human { @@ -1085,7 +1085,7 @@ mod explicit_async { } } - fn hero(&self) -> Box> { + fn hero(&self) -> Box> { let ch: Box> = match self { Self::Human => Box::new(Human { id: "human-32".to_string(), @@ -1318,8 +1318,8 @@ mod fallible_field { Droid, } - #[graphql_object(scalar = S)] - impl QueryRoot { + #[graphql_object(scalar = S: ScalarValue + Send + Sync)] + impl QueryRoot { fn character(&self) -> CharacterValue { match self { Self::Human => Human { @@ -1335,7 +1335,7 @@ mod fallible_field { } } - fn hero(&self) -> Box> { + fn hero(&self) -> Box> { let ch: Box> = match self { Self::Human => Box::new(Human { id: "human-32".to_string(), @@ -1584,8 +1584,8 @@ mod generic { Droid, } - #[graphql_object(scalar = S)] - impl QueryRoot { + #[graphql_object(scalar = S: ScalarValue + Send + Sync)] + impl QueryRoot { fn character(&self) -> CharacterValue { match self { Self::Human => Human { @@ -1601,7 +1601,7 @@ mod generic { } } - fn hero(&self) -> Box> { + fn hero(&self) -> Box> { let ch: Box> = match self { Self::Human => Box::new(Human { id: "human-32".to_string(), @@ -1725,6 +1725,7 @@ mod generic { ); } } + #[tokio::test] async fn dyn_resolves_info_field() { const DOC: &str = r#"{ @@ -1828,8 +1829,8 @@ mod generic_async { Droid, } - #[graphql_object(scalar = S)] - impl QueryRoot { + #[graphql_object(scalar = S: ScalarValue + Send + Sync)] + impl QueryRoot { fn character(&self) -> CharacterValue { match self { Self::Human => Human { @@ -1845,7 +1846,7 @@ mod generic_async { } } - fn hero(&self) -> Box> { + fn hero(&self) -> Box> { let ch: Box> = match self { Self::Human => Box::new(Human { id: "human-32".to_string(), @@ -1969,6 +1970,7 @@ mod generic_async { ); } } + #[tokio::test] async fn dyn_resolves_info_field() { const DOC: &str = r#"{ @@ -2072,8 +2074,8 @@ mod generic_lifetime_async { Droid, } - #[graphql_object(scalar = S)] - impl QueryRoot { + #[graphql_object(scalar = S: ScalarValue + Send + Sync)] + impl QueryRoot { fn character(&self) -> CharacterValue<'_, ()> { match self { Self::Human => Human { @@ -2089,7 +2091,7 @@ mod generic_lifetime_async { } } - fn hero(&self) -> Box> { + fn hero(&self) -> Box> { let ch: Box> = match self { Self::Human => Box::new(Human { id: "human-32".to_string(), @@ -2213,6 +2215,7 @@ mod generic_lifetime_async { ); } } + #[tokio::test] async fn dyn_resolves_info_field() { const DOC: &str = r#"{ @@ -2261,11 +2264,15 @@ mod argument { #[graphql_interface(for = Human)] trait Character { fn id_wide(&self, is_number: bool) -> &str; + + async fn id_wide2(&self, is_number: bool, r#async: Option) -> &str; } #[graphql_interface(dyn = DynHero, for = Human)] trait Hero { fn info_wide(&self, is_planet: bool) -> &str; + + async fn info_wide2(&self, is_planet: bool, r#async: Option) -> &str; } #[derive(GraphQLObject)] @@ -2284,6 +2291,14 @@ mod argument { "none" } } + + async fn id_wide2(&self, is_number: bool, _: Option) -> &str { + if is_number { + &self.id + } else { + "none" + } + } } #[graphql_interface(dyn)] @@ -2295,12 +2310,20 @@ mod argument { &self.id } } + + async fn info_wide2(&self, is_planet: bool, _: Option) -> &str { + if is_planet { + &self.home_planet + } else { + &self.id + } + } } struct QueryRoot; - #[graphql_object(scalar = S)] - impl QueryRoot { + #[graphql_object(scalar = S: ScalarValue + Send + Sync)] + impl QueryRoot { fn character(&self) -> CharacterValue { Human { id: "human-32".to_string(), @@ -2309,7 +2332,7 @@ mod argument { .into() } - fn hero(&self) -> Box> { + fn hero(&self) -> Box> { Box::new(Human { id: "human-32".to_string(), home_planet: "earth".to_string(), @@ -2322,14 +2345,26 @@ mod argument { let schema = schema(QueryRoot); for (input, expected) in &[ - ("{ character { idWide(isNumber: true) } }", "human-32"), - ("{ character { idWide(isNumber: false) } }", "none"), + ( + "{ character { idWide(isNumber: true), idWide2(isNumber: true) } }", + "human-32", + ), + ( + "{ character { idWide(isNumber: false), idWide2(isNumber: false, async: 5) } }", + "none", + ), ] { let expected: &str = *expected; assert_eq!( execute(*input, None, &schema, &Variables::new(), &()).await, - Ok((graphql_value!({"character": {"idWide": expected}}), vec![])), + Ok(( + graphql_value!({"character": { + "idWide": expected, + "idWide2": expected, + }}), + vec![], + )), ); } } @@ -2339,14 +2374,26 @@ mod argument { let schema = schema(QueryRoot); for (input, expected) in &[ - ("{ hero { infoWide(isPlanet: true) } }", "earth"), - ("{ hero { infoWide(isPlanet: false) } }", "human-32"), + ( + "{ hero { infoWide(isPlanet: true), infoWide2(isPlanet: true) } }", + "earth", + ), + ( + "{ hero { infoWide(isPlanet: false), infoWide2(isPlanet: false, async: 3) } }", + "human-32", + ), ] { let expected: &str = *expected; assert_eq!( execute(*input, None, &schema, &Variables::new(), &()).await, - Ok((graphql_value!({"hero": {"infoWide": expected}}), vec![])), + Ok(( + graphql_value!({"hero": { + "infoWide": expected, + "infoWide2": expected, + }}), + vec![], + )), ); } } @@ -2374,13 +2421,23 @@ mod argument { ); let expected_field_name: &str = *field; + let expected_field_name2: &str = &format!("{}2", field); let expected_arg_name: &str = *arg; assert_eq!( execute(&doc, None, &schema, &Variables::new(), &()).await, Ok(( - graphql_value!({"__type": {"fields": [ - {"name": expected_field_name, "args": [{"name": expected_arg_name}]}, - ]}}), + graphql_value!({"__type": {"fields": [{ + "name": expected_field_name, + "args": [ + {"name": expected_arg_name}, + ], + }, { + "name": expected_field_name2, + "args": [ + {"name": expected_arg_name}, + {"name": "async"}, + ], + }]}}), vec![], )), ); @@ -2408,7 +2465,10 @@ mod argument { assert_eq!( execute(&doc, None, &schema, &Variables::new(), &()).await, Ok(( - graphql_value!({"__type": { "fields": [{"args": [{"description": None}]}]}}), + graphql_value!({"__type": {"fields": [ + {"args": [{"description": None}]}, + {"args": [{"description": None}, {"description": None}]}, + ]}}), vec![], )), ); @@ -2436,7 +2496,10 @@ mod argument { assert_eq!( execute(&doc, None, &schema, &Variables::new(), &()).await, Ok(( - graphql_value!({"__type": { "fields": [{"args": [{"defaultValue": None}]}]}}), + graphql_value!({"__type": {"fields": [ + {"args": [{"defaultValue": None}]}, + {"args": [{"defaultValue": None}, {"defaultValue": None}]}, + ]}}), vec![], )), ); @@ -2447,6 +2510,11 @@ mod argument { mod default_argument { use super::*; + #[derive(GraphQLInputObject, Debug)] + struct Point { + x: i32, + } + #[graphql_interface(for = Human)] trait Character { async fn id( @@ -2455,12 +2523,17 @@ mod default_argument { #[graphql(default = "second".to_string())] second: String, #[graphql(default = "t")] third: String, ) -> String; + + fn info(&self, #[graphql(default = Point { x: 1 })] coord: Point) -> i32 { + coord.x + } } #[derive(GraphQLObject)] #[graphql(impl = CharacterValue)] struct Human { id: String, + info: i32, } #[graphql_interface] @@ -2477,6 +2550,7 @@ mod default_argument { fn character(&self) -> CharacterValue { Human { id: "human-32".to_string(), + info: 0, } .into() } @@ -2508,6 +2582,23 @@ mod default_argument { } } + #[tokio::test] + async fn resolves_info_field() { + let schema = schema(QueryRoot); + + for (input, expected) in &[ + ("{ character { info } }", 1), + ("{ character { info(coord: {x: 2}) } }", 2), + ] { + let expected: i32 = *expected; + + assert_eq!( + execute(*input, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"character": {"info": expected}}), vec![])), + ); + } + } + #[tokio::test] async fn has_defaults() { const DOC: &str = r#"{ @@ -2516,6 +2607,12 @@ mod default_argument { args { name defaultValue + type { + name + ofType { + name + } + } } } } @@ -2526,11 +2623,27 @@ mod default_argument { assert_eq!( execute(DOC, None, &schema, &Variables::new(), &()).await, Ok(( - graphql_value!({"__type": {"fields": [{"args": [ - {"name": "first", "defaultValue": r#""""#}, - {"name": "second", "defaultValue": r#""second""#}, - {"name": "third", "defaultValue": r#""t""#}, - ]}]}}), + graphql_value!({"__type": {"fields": [{ + "args": [{ + "name": "first", + "defaultValue": r#""""#, + "type": {"name": "String", "ofType": None}, + }, { + "name": "second", + "defaultValue": r#""second""#, + "type": {"name": "String", "ofType": None}, + }, { + "name": "third", + "defaultValue": r#""t""#, + "type": {"name": "String", "ofType": None}, + }], + }, { + "args": [{ + "name": "coord", + "defaultValue": "{x: 1}", + "type": {"name": "Point", "ofType": None}, + }], + }]}}), vec![], )), ); @@ -2544,6 +2657,7 @@ mod description_from_doc_comment { #[graphql_interface(for = Human)] trait Character { /// Rust `id` docs. + /// Long. fn id(&self) -> &str; } @@ -2591,7 +2705,8 @@ mod description_from_doc_comment { execute(DOC, None, &schema, &Variables::new(), &()).await, Ok(( graphql_value!({"__type": { - "description": "Rust docs.", "fields": [{"description": "Rust `id` docs."}], + "description": "Rust docs.", + "fields": [{"description": "Rust `id` docs.\nLong."}], }}), vec![], )), @@ -2600,8 +2715,6 @@ mod description_from_doc_comment { } mod deprecation_from_attr { - #![allow(deprecated)] - use super::*; #[graphql_interface(for = Human)] @@ -2675,7 +2788,7 @@ mod deprecation_from_attr { assert_eq!( execute(DOC, None, &schema, &Variables::new(), &()).await, - Ok((graphql_value!({"character": {"a": "a", "b": "b"}}), vec![],)), + Ok((graphql_value!({"character": {"a": "a", "b": "b"}}), vec![])), ); } @@ -2733,8 +2846,6 @@ mod deprecation_from_attr { } mod explicit_name_description_and_deprecation { - #![allow(deprecated)] - use super::*; /// Rust docs. @@ -2743,10 +2854,7 @@ mod explicit_name_description_and_deprecation { /// Rust `id` docs. #[graphql(name = "myId", desc = "My character ID.", deprecated = "Not used.")] #[deprecated(note = "Should be omitted.")] - fn id( - &self, - #[graphql(name = "myName", desc = "My argument.", default)] n: Option, - ) -> &str; + fn id(&self, #[graphql(name = "myName", desc = "My argument.")] n: Option) -> &str; #[graphql(deprecated)] #[deprecated(note = "Should be omitted.")] @@ -2918,6 +3026,107 @@ mod explicit_name_description_and_deprecation { } } +mod renamed_all_fields_and_args { + use super::*; + + #[graphql_interface(rename_all = "none", for = Human)] + trait Character { + fn id(&self) -> &str { + "human-32" + } + + async fn home_planet(&self, planet_name: String) -> String { + planet_name + } + + async fn r#async_info(&self, r#my_num: i32) -> i32 { + r#my_num + } + } + + struct Human; + + #[graphql_object(rename_all = "none", impl = CharacterValue)] + impl Human { + fn id() -> &'static str { + "human-32" + } + + fn home_planet(planet_name: String) -> String { + planet_name + } + + fn r#async_info(r#my_num: i32) -> i32 { + r#my_num + } + } + + #[graphql_interface] + impl Character for Human {} + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> CharacterValue { + Human.into() + } + } + + #[tokio::test] + async fn resolves_fields() { + const DOC: &str = r#"{ + character { + id + home_planet(planet_name: "earth") + async_info(my_num: 3) + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": { + "id": "human-32", + "home_planet": "earth", + "async_info": 3, + }}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_correct_fields_and_args_names() { + const DOC: &str = r#"{ + __type(name: "Character") { + fields { + name + args { + name + } + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [ + {"name": "id", "args": []}, + {"name": "home_planet", "args": [{"name": "planet_name"}]}, + {"name": "async_info", "args": [{"name": "my_num"}]}, + ]}}), + vec![], + )), + ); + } +} + mod explicit_scalar { use super::*; @@ -3430,9 +3639,9 @@ mod explicit_generic_scalar { Droid, } - #[graphql_object(scalar = S)] - impl QueryRoot { - fn character(&self) -> CharacterValue { + #[graphql_object(scalar = S: ScalarValue + Send + Sync)] + impl QueryRoot { + fn character(&self) -> CharacterValue { match self { Self::Human => Human { id: "human-32".to_string(), @@ -3447,7 +3656,230 @@ mod explicit_generic_scalar { } } - fn hero(&self) -> Box> { + fn hero(&self) -> Box> { + let ch: Box> = match self { + Self::Human => Box::new(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Box::new(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + }; + ch + } + } + + #[tokio::test] + async fn enum_resolves_human() { + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + } + }"#; + + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn enum_resolves_droid() { + const DOC: &str = r#"{ + character { + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + let schema = schema(QueryRoot::Droid); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn dyn_resolves_human() { + const DOC: &str = r#"{ + hero { + ... on Human { + humanId: id + homePlanet + } + } + }"#; + + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"hero": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn dyn_resolves_droid() { + const DOC: &str = r#"{ + hero { + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + let schema = schema(QueryRoot::Droid); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"hero": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn enum_resolves_id_field() { + const DOC: &str = r#"{ + character { + id + } + }"#; + + for (root, expected_id) in &[ + (QueryRoot::Human, "human-32"), + (QueryRoot::Droid, "droid-99"), + ] { + let schema = schema(*root); + + let expected_id: &str = *expected_id; + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"character": {"id": expected_id}}), vec![])), + ); + } + } + + #[tokio::test] + async fn dyn_resolves_info_field() { + const DOC: &str = r#"{ + hero { + info + } + }"#; + + for (root, expected_info) in &[(QueryRoot::Human, "earth"), (QueryRoot::Droid, "run")] { + let schema = schema(*root); + + let expected_info: &str = *expected_info; + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"hero": {"info": expected_info}}), vec![])), + ); + } + } +} + +mod bounded_generic_scalar { + use super::*; + + #[graphql_interface(for = [Human, Droid], scalar = S: ScalarValue + Clone)] + trait Character { + fn id(&self) -> &str; + } + + #[graphql_interface(dyn = DynHero, for = [Human, Droid], scalar = S: ScalarValue + Clone)] + trait Hero { + async fn info(&self) -> &str; + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero], scalar = S: ScalarValue + Clone)] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface(scalar = S: ScalarValue + Clone)] + impl Character for Human { + fn id(&self) -> &str { + &self.id + } + } + + #[graphql_interface(dyn, scalar = S: ScalarValue + Clone)] + impl Hero for Human { + async fn info(&self) -> &str { + &self.home_planet + } + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero<__S>])] + struct Droid { + id: String, + primary_function: String, + } + + #[graphql_interface(scalar = S: ScalarValue + Clone)] + impl Character for Droid { + fn id(&self) -> &str { + &self.id + } + } + + #[graphql_interface(dyn, scalar = S: ScalarValue + Clone)] + impl Hero for Droid { + async fn info(&self) -> &str { + &self.primary_function + } + } + + #[derive(Clone, Copy)] + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(scalar = S: ScalarValue + Clone + Send + Sync)] + impl QueryRoot { + fn character(&self) -> CharacterValue { + match self { + Self::Human => Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + } + .into(), + Self::Droid => Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + } + .into(), + } + } + + fn hero(&self) -> Box> { let ch: Box> = match self { Self::Human => Box::new(Human { id: "human-32".to_string(), @@ -3595,7 +4027,7 @@ mod explicit_generic_scalar { mod explicit_custom_context { use super::*; - pub struct CustomContext; + struct CustomContext; impl juniper::Context for CustomContext {} @@ -3698,8 +4130,8 @@ mod explicit_custom_context { Droid, } - #[graphql_object(context = CustomContext, scalar = S)] - impl QueryRoot { + #[graphql_object(context = CustomContext, scalar = S: ScalarValue + Send + Sync)] + impl QueryRoot { fn character(&self) -> CharacterValue { match self { Self::Human => Human { @@ -3715,7 +4147,7 @@ mod explicit_custom_context { } } - fn hero(&self) -> Box> { + fn hero(&self) -> Box> { let ch: Box> = match self { Self::Human => Box::new(Human { id: "human-32".to_string(), @@ -3862,7 +4294,7 @@ mod explicit_custom_context { mod inferred_custom_context_from_field { use super::*; - pub struct CustomContext(String); + struct CustomContext(String); impl juniper::Context for CustomContext {} @@ -3944,8 +4376,8 @@ mod inferred_custom_context_from_field { Droid, } - #[graphql_object(context = CustomContext, scalar = S)] - impl QueryRoot { + #[graphql_object(context = CustomContext, scalar = S: ScalarValue + Send + Sync)] + impl QueryRoot { fn character(&self) -> CharacterValue { match self { Self::Human => Human { @@ -3961,7 +4393,7 @@ mod inferred_custom_context_from_field { } } - fn hero(&self) -> Box> { + fn hero(&self) -> Box> { let ch: Box> = match self { Self::Human => Box::new(Human { id: "human-32".to_string(), @@ -4196,8 +4628,8 @@ mod inferred_custom_context_from_downcast { Droid, } - #[graphql_object(context = Database, scalar = S)] - impl QueryRoot { + #[graphql_object(context = Database, scalar = S: ScalarValue + Send + Sync)] + impl QueryRoot { fn character(&self) -> CharacterValue { match self { Self::Human => Human { @@ -4213,7 +4645,7 @@ mod inferred_custom_context_from_downcast { } } - fn hero(&self) -> Box> { + fn hero(&self) -> Box> { let ch: Box> = match self { Self::Human => Box::new(Human { id: "human-32".to_string(), @@ -4390,6 +4822,7 @@ mod executor { async fn info<'b>( &'b self, + arg: Option, #[graphql(executor)] another: &Executor<'_, '_, (), S>, ) -> &'b str where @@ -4407,6 +4840,7 @@ mod executor { async fn info<'b>( &'b self, + arg: Option, #[graphql(executor)] another: &Executor<'_, '_, (), S>, ) -> &'b str where @@ -4422,7 +4856,7 @@ mod executor { #[graphql_interface(scalar = S)] impl Character for Human { - async fn info<'b>(&'b self, _: &Executor<'_, '_, (), S>) -> &'b str + async fn info<'b>(&'b self, _: Option, _: &Executor<'_, '_, (), S>) -> &'b str where S: Send + Sync, { @@ -4432,7 +4866,7 @@ mod executor { #[graphql_interface(dyn, scalar = S)] impl Hero for Human { - async fn info<'b>(&'b self, _: &Executor<'_, '_, (), S>) -> &'b str + async fn info<'b>(&'b self, _: Option, _: &Executor<'_, '_, (), S>) -> &'b str where S: Send + Sync, { @@ -4449,7 +4883,7 @@ mod executor { #[graphql_interface(scalar = S)] impl Character for Droid { - async fn info<'b>(&'b self, _: &Executor<'_, '_, (), S>) -> &'b str + async fn info<'b>(&'b self, _: Option, _: &Executor<'_, '_, (), S>) -> &'b str where S: Send + Sync, { @@ -4459,7 +4893,7 @@ mod executor { #[graphql_interface(dyn, scalar = S)] impl Hero for Droid { - async fn info<'b>(&'b self, _: &Executor<'_, '_, (), S>) -> &'b str + async fn info<'b>(&'b self, _: Option, _: &Executor<'_, '_, (), S>) -> &'b str where S: Send + Sync, { @@ -4622,6 +5056,38 @@ mod executor { } } } + + #[tokio::test] + async fn not_arg() { + for interface in &["Character", "Hero"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + fields {{ + name + args {{ + name + }} + }} + }} + }}"#, + interface, + ); + + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [ + {"name": "id", "args": []}, + {"name": "info", "args": [{"name": "arg"}]}, + ]}}), + vec![], + )), + ); + } + } } mod ignored_method { @@ -4806,8 +5272,8 @@ mod downcast_method { Droid, } - #[graphql_object(scalar = S)] - impl QueryRoot { + #[graphql_object(scalar = S: ScalarValue + Send + Sync)] + impl QueryRoot { fn character(&self) -> CharacterValue { match self { Self::Human => Human { @@ -4823,7 +5289,7 @@ mod downcast_method { } } - fn hero(&self) -> Box> { + fn hero(&self) -> Box> { let ch: Box> = match self { Self::Human => Box::new(Human { id: "human-32".to_string(), @@ -5071,8 +5537,8 @@ mod external_downcast { Droid, } - #[graphql_object(context = Database, scalar = S)] - impl QueryRoot { + #[graphql_object(context = Database, scalar = S: ScalarValue + Send + Sync)] + impl QueryRoot { fn character(&self) -> CharacterValue { match self { Self::Human => Human { @@ -5088,7 +5554,7 @@ mod external_downcast { } } - fn hero(&self) -> Box> { + fn hero(&self) -> Box> { let ch: Box> = match self { Self::Human => Box::new(Human { id: "human-32".to_string(), diff --git a/integration_tests/juniper_tests/src/codegen/mod.rs b/integration_tests/juniper_tests/src/codegen/mod.rs index 0b45de8d3..6348a66c8 100644 --- a/integration_tests/juniper_tests/src/codegen/mod.rs +++ b/integration_tests/juniper_tests/src/codegen/mod.rs @@ -1,11 +1,12 @@ mod derive_enum; mod derive_input_object; -mod derive_object; mod derive_object_with_raw_idents; mod derive_scalar; -mod impl_object; mod impl_scalar; mod interface_attr; +mod object_attr; +mod object_derive; mod scalar_value_transparent; +mod subscription_attr; mod union_attr; mod union_derive; diff --git a/integration_tests/juniper_tests/src/codegen/object_attr.rs b/integration_tests/juniper_tests/src/codegen/object_attr.rs new file mode 100644 index 000000000..797a87fc8 --- /dev/null +++ b/integration_tests/juniper_tests/src/codegen/object_attr.rs @@ -0,0 +1,2124 @@ +//! Tests for `#[graphql_object]` macro. + +use juniper::{ + execute, graphql_object, graphql_value, DefaultScalarValue, EmptyMutation, EmptySubscription, + Executor, FieldError, FieldResult, GraphQLInputObject, GraphQLObject, GraphQLType, + IntoFieldError, RootNode, ScalarValue, Variables, +}; + +fn schema<'q, C, Q>(query_root: Q) -> RootNode<'q, Q, EmptyMutation, EmptySubscription> +where + Q: GraphQLType + 'q, +{ + RootNode::new( + query_root, + EmptyMutation::::new(), + EmptySubscription::::new(), + ) +} + +fn schema_with_scalar<'q, S, C, Q>( + query_root: Q, +) -> RootNode<'q, Q, EmptyMutation, EmptySubscription, S> +where + Q: GraphQLType + 'q, + S: ScalarValue + 'q, +{ + RootNode::new_with_scalar_value( + query_root, + EmptyMutation::::new(), + EmptySubscription::::new(), + ) +} + +mod trivial { + use super::*; + + struct Human; + + #[graphql_object] + impl Human { + fn id() -> &'static str { + "human-32" + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human() -> Human { + Human + } + } + + #[tokio::test] + async fn resolves() { + const DOC: &str = r#"{ + human { + id + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"human": {"id": "human-32"}}), vec![])), + ); + } + + #[tokio::test] + async fn is_graphql_object() { + const DOC: &str = r#"{ + __type(name: "Human") { + kind + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"kind": "OBJECT"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_name() { + const DOC: &str = r#"{ + __type(name: "Human") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "Human"}}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Human") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"description": None}}), vec![])), + ); + } +} + +mod trivial_async { + use futures::future; + + use super::*; + + struct Human; + + #[graphql_object] + impl Human { + async fn id() -> &'static str { + future::ready("human-32").await + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human() -> Human { + Human + } + } + + #[tokio::test] + async fn resolves() { + const DOC: &str = r#"{ + human { + id + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"human": {"id": "human-32"}}), vec![])), + ); + } + + #[tokio::test] + async fn is_graphql_object() { + const DOC: &str = r#"{ + __type(name: "Human") { + kind + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"kind": "OBJECT"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_name() { + const DOC: &str = r#"{ + __type(name: "Human") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "Human"}}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Human") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"description": None}}), vec![])), + ); + } +} + +mod raw_method { + use super::*; + + struct Human; + + #[graphql_object] + impl Human { + fn r#my_id() -> &'static str { + "human-32" + } + + async fn r#async() -> &'static str { + "async-32" + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human() -> Human { + Human + } + } + + #[tokio::test] + async fn resolves() { + const DOC: &str = r#"{ + human { + myId + async + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"human": {"myId": "human-32", "async": "async-32"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn has_correct_name() { + const DOC: &str = r#"{ + __type(name: "Human") { + name + kind + fields { + name + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": { + "name": "Human", + "kind": "OBJECT", + "fields": [{"name": "myId"}, {"name": "async"}], + }}), + vec![], + )), + ); + } +} + +mod ignored_method { + use super::*; + + struct Human; + + #[graphql_object] + impl Human { + fn id() -> &'static str { + "human-32" + } + + #[allow(dead_code)] + #[graphql(ignore)] + fn planet() -> &'static str { + "earth" + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human() -> Human { + Human + } + } + + #[tokio::test] + async fn resolves() { + const DOC: &str = r#"{ + human { + id + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"human": {"id": "human-32"}}), vec![])), + ); + } + + #[tokio::test] + async fn is_not_field() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields { + name + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [{"name": "id"}]}}), + vec![], + )), + ); + } +} + +mod fallible_method { + use super::*; + + struct CustomError; + + impl IntoFieldError for CustomError { + fn into_field_error(self) -> FieldError { + juniper::FieldError::new("Whatever", graphql_value!({"code": "some"})) + } + } + + struct Human { + id: String, + } + + #[graphql_object] + impl Human { + fn id(&self) -> Result<&str, CustomError> { + Ok(&self.id) + } + + async fn home_planet<__S>() -> FieldResult<&'static str, __S> { + Ok("earth") + } + } + + #[derive(Clone, Copy)] + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human() -> Human { + Human { + id: "human-32".to_string(), + } + } + } + + #[tokio::test] + async fn resolves() { + const DOC: &str = r#"{ + human { + id + homePlanet + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"human": {"id": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn has_correct_graphql_type() { + const DOC: &str = r#"{ + __type(name: "Human") { + name + kind + fields { + name + type { + kind + ofType { + name + } + } + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": { + "name": "Human", + "kind": "OBJECT", + "fields": [{ + "name": "id", + "type": {"kind": "NON_NULL", "ofType": {"name": "String"}}, + }, { + "name": "homePlanet", + "type": {"kind": "NON_NULL", "ofType": {"name": "String"}}, + }] + }}), + vec![], + )), + ); + } +} + +mod generic { + use super::*; + + struct Human { + id: A, + _home_planet: B, + } + + #[graphql_object] + impl Human { + fn id(&self) -> i32 { + self.id + } + } + + #[graphql_object(name = "HumanString")] + impl Human { + fn id(&self) -> &str { + self.id.as_str() + } + } + + #[derive(Clone, Copy)] + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human(&self) -> Human { + Human { + id: 32, + _home_planet: (), + } + } + + fn human_string(&self) -> Human { + Human { + id: "human-32".into(), + _home_planet: (), + } + } + } + + #[tokio::test] + async fn resolves_human() { + const DOC: &str = r#"{ + human { + id + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"human": {"id": 32}}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_human_string() { + const DOC: &str = r#"{ + humanString { + id + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"humanString": {"id": "human-32"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_name_without_type_params() { + const DOC: &str = r#"{ + __type(name: "Human") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "Human"}}), vec![])), + ); + } +} + +mod generic_async { + use super::*; + + struct Human { + id: A, + _home_planet: B, + } + + #[graphql_object] + impl Human { + async fn id(&self) -> i32 { + self.id + } + } + + #[graphql_object(name = "HumanString")] + impl Human { + async fn id(&self) -> &str { + self.id.as_str() + } + } + + #[derive(Clone, Copy)] + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human(&self) -> Human { + Human { + id: 32, + _home_planet: (), + } + } + + fn human_string(&self) -> Human { + Human { + id: "human-32".into(), + _home_planet: (), + } + } + } + + #[tokio::test] + async fn resolves_human() { + const DOC: &str = r#"{ + human { + id + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"human": {"id": 32}}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_human_string() { + const DOC: &str = r#"{ + humanString { + id + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"humanString": {"id": "human-32"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_name_without_type_params() { + const DOC: &str = r#"{ + __type(name: "Human") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "Human"}}), vec![])), + ); + } +} + +mod generic_lifetime_async { + use super::*; + + struct Human<'p, A = ()> { + id: A, + home_planet: &'p str, + } + + #[graphql_object] + impl<'p> Human<'p, i32> { + async fn id(&self) -> i32 { + self.id + } + + async fn planet(&self) -> &str { + self.home_planet + } + } + + #[graphql_object(name = "HumanString")] + impl<'id, 'p> Human<'p, &'id str> { + async fn id(&self) -> &str { + self.id + } + + async fn planet(&self) -> &str { + self.home_planet + } + } + + #[derive(Clone)] + struct QueryRoot(String); + + #[graphql_object] + impl QueryRoot { + fn human(&self) -> Human<'static, i32> { + Human { + id: 32, + home_planet: "earth", + } + } + + fn human_string(&self) -> Human<'_, &str> { + Human { + id: self.0.as_str(), + home_planet: self.0.as_str(), + } + } + } + + #[tokio::test] + async fn resolves_human() { + const DOC: &str = r#"{ + human { + id + planet + } + }"#; + + let schema = schema(QueryRoot("mars".into())); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"human": {"id": 32, "planet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_human_string() { + const DOC: &str = r#"{ + humanString { + id + planet + } + }"#; + + let schema = schema(QueryRoot("mars".into())); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"humanString": {"id": "mars", "planet": "mars"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_type_name_without_type_params() { + const DOC: &str = r#"{ + __type(name: "Human") { + name + } + }"#; + + let schema = schema(QueryRoot("mars".into())); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "Human"}}), vec![])), + ); + } +} + +mod nested_generic_lifetime_async { + use super::*; + + struct Droid<'p, A = ()> { + id: A, + primary_function: &'p str, + } + + #[graphql_object] + impl<'p> Droid<'p, i32> { + async fn id(&self) -> i32 { + self.id + } + + async fn primary_function(&self) -> &str { + self.primary_function + } + } + + #[graphql_object(name = "DroidString")] + impl<'id, 'p> Droid<'p, &'id str> { + async fn id(&self) -> &str { + self.id + } + + async fn primary_function(&self) -> &str { + self.primary_function + } + } + + struct Human<'p, A = ()> { + id: A, + home_planet: &'p str, + } + + #[graphql_object] + impl<'p> Human<'p, i32> { + async fn id(&self) -> i32 { + self.id + } + + async fn planet(&self) -> &str { + self.home_planet + } + + async fn droid(&self) -> Droid<'_, i32> { + Droid { + id: self.id, + primary_function: "run", + } + } + } + + #[graphql_object(name = "HumanString")] + impl<'id, 'p> Human<'p, &'id str> { + async fn id(&self) -> &str { + self.id + } + + async fn planet(&self) -> &str { + self.home_planet + } + + async fn droid(&self) -> Droid<'_, &str> { + Droid { + id: "none", + primary_function: self.home_planet, + } + } + } + + #[derive(Clone)] + struct QueryRoot(String); + + #[graphql_object] + impl QueryRoot { + fn human(&self) -> Human<'static, i32> { + Human { + id: 32, + home_planet: "earth", + } + } + + fn human_string(&self) -> Human<'_, &str> { + Human { + id: self.0.as_str(), + home_planet: self.0.as_str(), + } + } + } + + #[tokio::test] + async fn resolves_human() { + const DOC: &str = r#"{ + human { + id + planet + droid { + id + primaryFunction + } + } + }"#; + + let schema = schema(QueryRoot("mars".into())); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"human": { + "id": 32, + "planet": "earth", + "droid": { + "id": 32, + "primaryFunction": "run", + }, + }}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_human_string() { + const DOC: &str = r#"{ + humanString { + id + planet + droid { + id + primaryFunction + } + } + }"#; + + let schema = schema(QueryRoot("mars".into())); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"humanString": { + "id": "mars", + "planet": "mars", + "droid": { + "id": "none", + "primaryFunction": "mars", + }, + }}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_type_name_without_type_params() { + for object in &["Human", "Droid"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + name + }} + }}"#, + object, + ); + + let schema = schema(QueryRoot("mars".into())); + + let expected_name: &str = *object; + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": expected_name}}), vec![])), + ); + } + } +} + +mod argument { + use super::*; + + struct Human; + + #[graphql_object] + impl Human { + fn id(arg: String) -> String { + arg + } + + async fn home_planet(&self, r#raw_arg: String, r#async: Option) -> String { + format!("{},{:?}", r#raw_arg, r#async) + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human() -> Human { + Human + } + } + + #[tokio::test] + async fn resolves() { + const DOC: &str = r#"{ + human { + id(arg: "human-32") + homePlanet(rawArg: "earth") + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"human": {"id": "human-32", "homePlanet": "earth,None"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn has_correct_name() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields { + name + args { + name + } + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [ + {"name": "id", "args": [{"name": "arg"}]}, + {"name": "homePlanet", "args": [{"name": "rawArg"}, {"name": "async"}]}, + ]}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields { + args { + description + } + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [ + {"args": [{"description": None}]}, + {"args": [{"description": None}, {"description": None}]}, + ]}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn has_no_defaults() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields { + args { + defaultValue + } + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [ + {"args": [{"defaultValue": None}]}, + {"args": [{"defaultValue": None}, {"defaultValue": None}]}, + ]}}), + vec![], + )), + ); + } +} + +mod default_argument { + use super::*; + + #[derive(GraphQLInputObject, Debug)] + struct Point { + x: i32, + } + + struct Human; + + #[graphql_object] + impl Human { + fn id( + #[graphql(default)] arg1: i32, + #[graphql(default = "second".to_string())] arg2: String, + #[graphql(default = true)] r#arg3: bool, + ) -> String { + format!("{}|{}&{}", arg1, arg2, r#arg3) + } + + fn info(#[graphql(default = Point { x: 1 })] coord: Point) -> i32 { + coord.x + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human() -> Human { + Human + } + } + + #[tokio::test] + async fn resolves_id_field() { + let schema = schema(QueryRoot); + + for (input, expected) in &[ + ("{ human { id } }", "0|second&true"), + ("{ human { id(arg1: 1) } }", "1|second&true"), + (r#"{ human { id(arg2: "") } }"#, "0|&true"), + (r#"{ human { id(arg1: 2, arg2: "") } }"#, "2|&true"), + ( + r#"{ human { id(arg1: 1, arg2: "", arg3: false) } }"#, + "1|&false", + ), + ] { + let expected: &str = *expected; + + assert_eq!( + execute(*input, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"human": {"id": expected}}), vec![])), + ); + } + } + + #[tokio::test] + async fn resolves_info_field() { + let schema = schema(QueryRoot); + + for (input, expected) in &[ + ("{ human { info } }", 1), + ("{ human { info(coord: { x: 2 }) } }", 2), + ] { + let expected: i32 = *expected; + + assert_eq!( + execute(*input, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"human": {"info": expected}}), vec![])), + ); + } + } + + #[tokio::test] + async fn has_defaults() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields { + args { + name + defaultValue + type { + name + ofType { + name + } + } + } + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [{ + "args": [{ + "name": "arg1", + "defaultValue": "0", + "type": {"name": "Int", "ofType": None}, + }, { + "name": "arg2", + "defaultValue": r#""second""#, + "type": {"name": "String", "ofType": None}, + }, { + "name": "arg3", + "defaultValue": "true", + "type": {"name": "Boolean", "ofType": None}, + }], + }, { + "args": [{ + "name": "coord", + "defaultValue": "{x: 1}", + "type": {"name": "Point", "ofType": None}, + }], + }]}}), + vec![], + )), + ); + } +} + +mod description_from_doc_comment { + use super::*; + + struct Human; + + /// Rust docs. + #[graphql_object] + impl Human { + /// Rust `id` docs. + fn id() -> &'static str { + "human-32" + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human() -> Human { + Human + } + } + + #[tokio::test] + async fn uses_doc_comment_as_description() { + const DOC: &str = r#"{ + __type(name: "Human") { + description + fields { + description + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": { + "description": "Rust docs.", + "fields": [{"description": "Rust `id` docs."}], + }}), + vec![], + )), + ); + } +} + +mod deprecation_from_attr { + use super::*; + + struct Human; + + #[graphql_object] + impl Human { + fn id() -> &'static str { + "human-32" + } + + #[deprecated] + fn a(&self) -> &str { + "a" + } + + #[deprecated(note = "Use `id`.")] + fn b(&self) -> &str { + "b" + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human() -> Human { + Human + } + } + + #[tokio::test] + async fn resolves_id_field() { + const DOC: &str = r#"{ + human { + id + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"human": {"id": "human-32"}}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_deprecated_fields() { + const DOC: &str = r#"{ + human { + a + b + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"human": {"a": "a", "b": "b"}}), vec![])), + ); + } + + #[tokio::test] + async fn deprecates_fields() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields(includeDeprecated: true) { + name + isDeprecated + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [ + {"name": "id", "isDeprecated": false}, + {"name": "a", "isDeprecated": true}, + {"name": "b", "isDeprecated": true}, + ]}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn provides_deprecation_reason() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields(includeDeprecated: true) { + name + deprecationReason + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [ + {"name": "id", "deprecationReason": None}, + {"name": "a", "deprecationReason": None}, + {"name": "b", "deprecationReason": "Use `id`."}, + ]}}), + vec![], + )), + ); + } +} + +mod explicit_name_description_and_deprecation { + use super::*; + + struct Human; + + /// Rust docs. + #[graphql_object(name = "MyHuman", desc = "My human.")] + impl Human { + /// Rust `id` docs. + #[graphql(name = "myId", desc = "My human ID.", deprecated = "Not used.")] + #[deprecated(note = "Should be omitted.")] + fn id( + #[graphql(name = "myName", desc = "My argument.", default)] _n: String, + ) -> &'static str { + "human-32" + } + + #[graphql(deprecated)] + #[deprecated(note = "Should be omitted.")] + fn a(&self) -> &str { + "a" + } + + fn b(&self) -> &str { + "b" + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human() -> Human { + Human + } + } + + #[tokio::test] + async fn resolves_fields() { + const DOC: &str = r#"{ + human { + myId + a + b + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"human": {"myId": "human-32", "a": "a", "b": "b"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_custom_name() { + const DOC: &str = r#"{ + __type(name: "MyHuman") { + name + fields(includeDeprecated: true) { + name + args { + name + } + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": { + "name": "MyHuman", + "fields": [ + {"name": "myId", "args": [{"name": "myName"}]}, + {"name": "a", "args": []}, + {"name": "b", "args": []}, + ], + }}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_custom_description() { + const DOC: &str = r#"{ + __type(name: "MyHuman") { + description + fields(includeDeprecated: true) { + name + description + args { + description + } + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": { + "description": "My human.", + "fields": [{ + "name": "myId", + "description": "My human ID.", + "args": [{"description": "My argument."}], + }, { + "name": "a", + "description": None, + "args": [], + }, { + "name": "b", + "description": None, + "args": [], + }], + }}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_custom_deprecation() { + const DOC: &str = r#"{ + __type(name: "MyHuman") { + fields(includeDeprecated: true) { + name + isDeprecated + deprecationReason + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": { + "fields": [{ + "name": "myId", + "isDeprecated": true, + "deprecationReason": "Not used.", + }, { + "name": "a", + "isDeprecated": true, + "deprecationReason": None, + }, { + "name": "b", + "isDeprecated": false, + "deprecationReason": None, + }], + }}), + vec![], + )), + ); + } +} + +mod renamed_all_fields_and_args { + use super::*; + + struct Human; + + #[graphql_object(rename_all = "none")] + impl Human { + fn id() -> &'static str { + "human-32" + } + + async fn home_planet(&self, planet_name: String) -> String { + planet_name + } + + async fn r#async_info(r#my_num: i32) -> i32 { + r#my_num + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human() -> Human { + Human + } + } + + #[tokio::test] + async fn resolves_fields() { + const DOC: &str = r#"{ + human { + id + home_planet(planet_name: "earth") + async_info(my_num: 3) + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"human": { + "id": "human-32", + "home_planet": "earth", + "async_info": 3, + }}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_correct_fields_and_args_names() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields { + name + args { + name + } + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [ + {"name": "id", "args": []}, + {"name": "home_planet", "args": [{"name": "planet_name"}]}, + {"name": "async_info", "args": [{"name": "my_num"}]}, + ]}}), + vec![], + )), + ); + } +} + +mod explicit_scalar { + use super::*; + + struct Human; + + #[graphql_object(scalar = DefaultScalarValue)] + impl Human { + fn id(&self) -> &str { + "human-32" + } + + async fn home_planet() -> &'static str { + "earth" + } + } + + struct QueryRoot; + + #[graphql_object(scalar = DefaultScalarValue)] + impl QueryRoot { + fn human() -> Human { + Human + } + } + + #[tokio::test] + async fn resolves_fields() { + const DOC: &str = r#"{ + human { + id + homePlanet + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"human": {"id": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } +} + +mod custom_scalar { + use crate::custom_scalar::MyScalarValue; + + use super::*; + + struct Human; + + #[graphql_object(scalar = MyScalarValue)] + impl Human { + fn id() -> &'static str { + "human-32" + } + + async fn home_planet(&self) -> &str { + "earth" + } + } + + struct QueryRoot; + + #[graphql_object(scalar = MyScalarValue)] + impl QueryRoot { + fn human() -> Human { + Human + } + } + + #[tokio::test] + async fn resolves_fields() { + const DOC: &str = r#"{ + human { + id + homePlanet + } + }"#; + + let schema = schema_with_scalar::(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"human": {"id": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } +} + +mod explicit_generic_scalar { + use std::marker::PhantomData; + + use super::*; + + struct Human(PhantomData); + + #[graphql_object(scalar = S)] + impl Human { + fn id() -> &'static str { + "human-32" + } + + async fn another(&self, _executor: &Executor<'_, '_, (), S>) -> Human { + Human(PhantomData) + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human<__S>() -> Human<__S> { + Human(PhantomData) + } + } + + #[tokio::test] + async fn resolves_fields() { + const DOC: &str = r#"{ + human { + id + another { + id + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"human": { + "id": "human-32", + "another": {"id": "human-32"}, + }}), + vec![], + )), + ); + } +} + +mod bounded_generic_scalar { + use super::*; + + struct Human; + + #[graphql_object(scalar = S: ScalarValue + Clone)] + impl Human { + fn id() -> &'static str { + "human-32" + } + + async fn another(&self, _executor: &Executor<'_, '_, (), S>) -> Human { + Human + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human() -> Human { + Human + } + } + + #[tokio::test] + async fn resolves_fields() { + const DOC: &str = r#"{ + human { + id + another { + id + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"human": { + "id": "human-32", + "another": {"id": "human-32"}, + }}), + vec![], + )), + ); + } +} + +mod explicit_custom_context { + use super::*; + + struct CustomContext(String); + + impl juniper::Context for CustomContext {} + + struct Human; + + #[graphql_object(context = CustomContext)] + impl Human { + async fn id<'c>(&self, context: &'c CustomContext) -> &'c str { + context.0.as_str() + } + + async fn info(_ctx: &()) -> &'static str { + "human being" + } + + fn more(#[graphql(context)] custom: &CustomContext) -> &str { + custom.0.as_str() + } + } + + struct QueryRoot; + + #[graphql_object(context = CustomContext)] + impl QueryRoot { + fn human() -> Human { + Human + } + } + + #[tokio::test] + async fn resolves_fields() { + const DOC: &str = r#"{ + human { + id + info + more + } + }"#; + + let schema = schema(QueryRoot); + let ctx = CustomContext("ctx!".into()); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &ctx).await, + Ok(( + graphql_value!({"human": { + "id": "ctx!", + "info": "human being", + "more": "ctx!", + }}), + vec![], + )), + ); + } +} + +mod inferred_custom_context_from_field { + use super::*; + + struct CustomContext(String); + + impl juniper::Context for CustomContext {} + + struct Human; + + #[graphql_object] + impl Human { + async fn id<'c>(&self, context: &'c CustomContext) -> &'c str { + context.0.as_str() + } + + async fn info(_ctx: &()) -> &'static str { + "human being" + } + + fn more(#[graphql(context)] custom: &CustomContext) -> &str { + custom.0.as_str() + } + } + + struct QueryRoot; + + #[graphql_object(context = CustomContext)] + impl QueryRoot { + fn human() -> Human { + Human + } + } + + #[tokio::test] + async fn resolves_fields() { + const DOC: &str = r#"{ + human { + id + info + more + } + }"#; + + let schema = schema(QueryRoot); + let ctx = CustomContext("ctx!".into()); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &ctx).await, + Ok(( + graphql_value!({"human": { + "id": "ctx!", + "info": "human being", + "more": "ctx!", + }}), + vec![], + )), + ); + } +} + +mod executor { + use juniper::LookAheadMethods as _; + + use super::*; + + struct Human; + + #[graphql_object(scalar = S: ScalarValue)] + impl Human { + async fn id<'e, S>(&self, executor: &'e Executor<'_, '_, (), S>) -> &'e str + where + S: ScalarValue, + { + executor.look_ahead().field_name() + } + + fn info( + &self, + arg: String, + #[graphql(executor)] _another: &Executor<'_, '_, (), S>, + ) -> String { + arg + } + + fn info2<'e, S>(_executor: &'e Executor<'_, '_, (), S>) -> &'e str { + "no info" + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human() -> Human { + Human + } + } + + #[tokio::test] + async fn resolves_fields() { + const DOC: &str = r#"{ + human { + id + info(arg: "input!") + info2 + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"human": { + "id": "id", + "info": "input!", + "info2": "no info", + }}), + vec![], + )), + ); + } + + #[tokio::test] + async fn not_arg() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields { + name + args { + name + } + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [ + {"name": "id", "args": []}, + {"name": "info", "args": [{"name": "arg"}]}, + {"name": "info2", "args": []}, + ]}}), + vec![], + )), + ); + } +} + +mod switched_context { + use super::*; + + struct CustomContext; + + impl juniper::Context for CustomContext {} + + #[derive(GraphQLObject)] + #[graphql(context = CustomContext)] + struct Droid { + id: i32, + } + + struct Human; + + #[graphql_object(context = CustomContext)] + impl Human { + fn switch_always<'e, S: ScalarValue>( + executor: &'e Executor<'_, '_, CustomContext, S>, + ) -> (&'e CustomContext, Droid) { + (executor.context(), Droid { id: 0 }) + } + + async fn switch_opt<'e, S: ScalarValue>( + executor: &'e Executor<'_, '_, CustomContext, S>, + ) -> Option<(&'e CustomContext, Droid)> { + Some((executor.context(), Droid { id: 1 })) + } + + fn switch_res<'e, S: ScalarValue>( + &self, + executor: &'e Executor<'_, '_, CustomContext, S>, + ) -> FieldResult<(&'e CustomContext, Droid)> { + Ok((executor.context(), Droid { id: 2 })) + } + + async fn switch_res_opt<'e, S: ScalarValue>( + &self, + executor: &'e Executor<'_, '_, CustomContext, S>, + ) -> FieldResult> { + Ok(Some((executor.context(), Droid { id: 3 }))) + } + } + + struct QueryRoot; + + #[graphql_object(context = CustomContext)] + impl QueryRoot { + fn human() -> Human { + Human + } + } + + #[tokio::test] + async fn resolves_fields() { + const DOC: &str = r#"{ + human { + switchAlways { id } + switchOpt { id } + switchRes { id } + switchResOpt { id } + } + }"#; + + let schema = schema(QueryRoot); + let ctx = CustomContext; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &ctx).await, + Ok(( + graphql_value!({"human": { + "switchAlways": {"id": 0}, + "switchOpt": {"id": 1}, + "switchRes": {"id": 2}, + "switchResOpt": {"id": 3}, + }}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_correct_fields_types() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields { + name + type { + kind + name + ofType { + name + } + } + } + } + }"#; + + let schema = schema(QueryRoot); + let ctx = CustomContext; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &ctx).await, + Ok(( + graphql_value!({"__type": {"fields": [{ + "name": "switchAlways", + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": {"name": "Droid"}, + }, + }, { + "name": "switchOpt", + "type": { + "kind": "OBJECT", + "name": "Droid", + "ofType": None, + }, + }, { + "name": "switchRes", + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": {"name": "Droid"}, + }, + }, { + "name": "switchResOpt", + "type": { + "kind": "OBJECT", + "name": "Droid", + "ofType": None, + }, + }]}}), + vec![], + )), + ); + } +} diff --git a/integration_tests/juniper_tests/src/codegen/object_derive.rs b/integration_tests/juniper_tests/src/codegen/object_derive.rs new file mode 100644 index 000000000..5162f9d95 --- /dev/null +++ b/integration_tests/juniper_tests/src/codegen/object_derive.rs @@ -0,0 +1,1007 @@ +//! Tests for `#[derive(GraphQLObject)]` macro. + +use juniper::{ + execute, graphql_object, graphql_value, DefaultScalarValue, EmptyMutation, EmptySubscription, + GraphQLObject, GraphQLType, RootNode, ScalarValue, Variables, +}; + +fn schema<'q, C, Q>(query_root: Q) -> RootNode<'q, Q, EmptyMutation, EmptySubscription> +where + Q: GraphQLType + 'q, +{ + RootNode::new( + query_root, + EmptyMutation::::new(), + EmptySubscription::::new(), + ) +} + +fn schema_with_scalar<'q, S, C, Q>( + query_root: Q, +) -> RootNode<'q, Q, EmptyMutation, EmptySubscription, S> +where + Q: GraphQLType + 'q, + S: ScalarValue + 'q, +{ + RootNode::new_with_scalar_value( + query_root, + EmptyMutation::::new(), + EmptySubscription::::new(), + ) +} + +mod trivial { + use super::*; + + #[derive(GraphQLObject)] + struct Human { + id: &'static str, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human() -> Human { + Human { id: "human-32" } + } + } + + #[tokio::test] + async fn resolves() { + const DOC: &str = r#"{ + human { + id + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"human": {"id": "human-32"}}), vec![])), + ); + } + + #[tokio::test] + async fn is_graphql_object() { + const DOC: &str = r#"{ + __type(name: "Human") { + kind + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"kind": "OBJECT"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_name() { + const DOC: &str = r#"{ + __type(name: "Human") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "Human"}}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Human") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"description": None}}), vec![])), + ); + } +} + +mod raw_field { + use super::*; + + #[derive(GraphQLObject)] + struct Human { + r#async: &'static str, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human() -> Human { + Human { + r#async: "human-32", + } + } + } + + #[tokio::test] + async fn resolves() { + const DOC: &str = r#"{ + human { + async + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"human": {"async": "human-32"}}), vec![])), + ); + } + + #[tokio::test] + async fn has_correct_name() { + const DOC: &str = r#"{ + __type(name: "Human") { + name + kind + fields { + name + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": { + "name": "Human", + "kind": "OBJECT", + "fields": [{"name": "async"}], + }}), + vec![], + )), + ); + } +} + +mod ignored_field { + use super::*; + + #[derive(GraphQLObject)] + struct Human { + id: &'static str, + #[allow(dead_code)] + #[graphql(ignore)] + planet: &'static str, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human() -> Human { + Human { + id: "human-32", + planet: "earth", + } + } + } + + #[tokio::test] + async fn resolves() { + const DOC: &str = r#"{ + human { + id + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"human": {"id": "human-32"}}), vec![])), + ); + } + + #[tokio::test] + async fn is_not_field() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields { + name + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [{"name": "id"}]}}), + vec![], + )), + ); + } +} + +mod generic { + use super::*; + + #[derive(GraphQLObject)] + struct Human { + id: &'static str, + #[graphql(ignore)] + _home_planet: B, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human() -> Human { + Human { + id: "human-32", + _home_planet: (), + } + } + } + + #[tokio::test] + async fn resolves() { + const DOC: &str = r#"{ + human { + id + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"human": {"id": "human-32"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_name_without_type_params() { + const DOC: &str = r#"{ + __type(name: "Human") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "Human"}}), vec![])), + ); + } +} + +mod generic_lifetime { + use super::*; + + #[derive(GraphQLObject)] + struct Human<'id, B: ?Sized = ()> { + id: &'id str, + #[graphql(ignore)] + _home_planet: B, + } + + struct QueryRoot(String); + + #[graphql_object] + impl QueryRoot { + fn human(&self) -> Human<'_, i32> { + Human { + id: self.0.as_str(), + _home_planet: 32, + } + } + } + + #[tokio::test] + async fn resolves() { + const DOC: &str = r#"{ + human { + id + } + }"#; + + let schema = schema(QueryRoot("mars".into())); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"human": {"id": "mars"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_name_without_type_params() { + const DOC: &str = r#"{ + __type(name: "Human") { + name + } + }"#; + + let schema = schema(QueryRoot("mars".into())); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "Human"}}), vec![])), + ); + } +} + +mod nested_generic_lifetime_async { + use super::*; + + #[derive(GraphQLObject)] + struct Droid<'p, A = ()> { + #[graphql(ignore)] + _id: A, + primary_function: &'p str, + } + + #[derive(GraphQLObject)] + struct Human<'d, A: Sync = ()> { + id: i32, + droid: Droid<'d, A>, + } + + struct QueryRoot(String); + + #[graphql_object] + impl QueryRoot { + fn human(&self) -> Human<'_, i8> { + Human { + id: 32, + droid: Droid { + _id: 12, + primary_function: self.0.as_str(), + }, + } + } + } + + #[tokio::test] + async fn resolves() { + const DOC: &str = r#"{ + human { + id + droid { + primaryFunction + } + } + }"#; + + let schema = schema(QueryRoot("mars".into())); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"human": { + "id": 32, + "droid": { + "primaryFunction": "mars", + }, + }}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_type_name_without_type_params() { + for object in &["Human", "Droid"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + name + }} + }}"#, + object, + ); + + let schema = schema(QueryRoot("mars".into())); + + let expected_name: &str = *object; + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": expected_name}}), vec![])), + ); + } + } +} + +mod description_from_doc_comment { + use super::*; + + /// Rust docs.\ + /// Here. + #[derive(GraphQLObject)] + struct Human { + /// Rust `id` docs. + /// Here. + id: String, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human() -> Human { + Human { + id: "human-32".into(), + } + } + } + + #[tokio::test] + async fn uses_doc_comment_as_description() { + const DOC: &str = r#"{ + __type(name: "Human") { + description + fields { + description + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": { + "description": "Rust docs. Here.", + "fields": [{"description": "Rust `id` docs.\nHere."}], + }}), + vec![], + )), + ); + } +} + +mod deprecation_from_attr { + use super::*; + + #[derive(GraphQLObject)] + struct Human { + id: String, + #[deprecated] + a: &'static str, + #[deprecated(note = "Use `id`.")] + b: &'static str, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + #[allow(deprecated)] + fn human() -> Human { + Human { + id: "human-32".into(), + a: "a", + b: "b", + } + } + } + + #[tokio::test] + async fn resolves_id_field() { + const DOC: &str = r#"{ + human { + id + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"human": {"id": "human-32"}}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_deprecated_fields() { + const DOC: &str = r#"{ + human { + a + b + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"human": {"a": "a", "b": "b"}}), vec![])), + ); + } + + #[tokio::test] + async fn deprecates_fields() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields(includeDeprecated: true) { + name + isDeprecated + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [ + {"name": "id", "isDeprecated": false}, + {"name": "a", "isDeprecated": true}, + {"name": "b", "isDeprecated": true}, + ]}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn provides_deprecation_reason() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields(includeDeprecated: true) { + name + deprecationReason + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [ + {"name": "id", "deprecationReason": None}, + {"name": "a", "deprecationReason": None}, + {"name": "b", "deprecationReason": "Use `id`."}, + ]}}), + vec![], + )), + ); + } +} + +mod explicit_name_description_and_deprecation { + use super::*; + + /// Rust docs. + #[derive(GraphQLObject)] + #[graphql(name = "MyHuman", desc = "My human.")] + struct Human { + /// Rust `id` docs. + #[graphql(name = "myId", desc = "My human ID.", deprecated = "Not used.")] + #[deprecated(note = "Should be omitted.")] + id: String, + #[graphql(deprecated)] + #[deprecated(note = "Should be omitted.")] + a: &'static str, + b: &'static str, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + #[allow(deprecated)] + fn human() -> Human { + Human { + id: "human-32".into(), + a: "a", + b: "b", + } + } + } + + #[tokio::test] + async fn resolves_fields() { + const DOC: &str = r#"{ + human { + myId + a + b + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"human": {"myId": "human-32", "a": "a", "b": "b"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_custom_name() { + const DOC: &str = r#"{ + __type(name: "MyHuman") { + name + fields(includeDeprecated: true) { + name + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": { + "name": "MyHuman", + "fields": [ + {"name": "myId"}, + {"name": "a"}, + {"name": "b"}, + ], + }}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_custom_description() { + const DOC: &str = r#"{ + __type(name: "MyHuman") { + description + fields(includeDeprecated: true) { + name + description + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": { + "description": "My human.", + "fields": [{ + "name": "myId", + "description": "My human ID.", + }, { + "name": "a", + "description": None, + }, { + "name": "b", + "description": None, + }], + }}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_custom_deprecation() { + const DOC: &str = r#"{ + __type(name: "MyHuman") { + fields(includeDeprecated: true) { + name + isDeprecated + deprecationReason + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": { + "fields": [{ + "name": "myId", + "isDeprecated": true, + "deprecationReason": "Not used.", + }, { + "name": "a", + "isDeprecated": true, + "deprecationReason": None, + }, { + "name": "b", + "isDeprecated": false, + "deprecationReason": None, + }], + }}), + vec![], + )), + ); + } +} + +mod renamed_all_fields { + use super::*; + + #[derive(GraphQLObject)] + #[graphql(rename_all = "none")] + struct Human { + id: &'static str, + home_planet: String, + r#async_info: i32, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human() -> Human { + Human { + id: "human-32", + home_planet: "earth".into(), + r#async_info: 3, + } + } + } + + #[tokio::test] + async fn resolves_fields() { + const DOC: &str = r#"{ + human { + id + home_planet + async_info + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"human": { + "id": "human-32", + "home_planet": "earth", + "async_info": 3, + }}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_correct_fields_names() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields { + name + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [ + {"name": "id"}, + {"name": "home_planet"}, + {"name": "async_info"}, + ]}}), + vec![], + )), + ); + } +} + +mod explicit_scalar { + use super::*; + + #[derive(GraphQLObject)] + #[graphql(scalar = DefaultScalarValue)] + struct Human { + id: &'static str, + } + + struct QueryRoot; + + #[graphql_object(scalar = DefaultScalarValue)] + impl QueryRoot { + fn human() -> Human { + Human { id: "human-32" } + } + } + + #[tokio::test] + async fn resolves_fields() { + const DOC: &str = r#"{ + human { + id + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"human": {"id": "human-32"}}), vec![])), + ); + } +} + +mod custom_scalar { + use crate::custom_scalar::MyScalarValue; + + use super::*; + + #[derive(GraphQLObject)] + #[graphql(scalar = MyScalarValue)] + struct Human { + id: &'static str, + } + + struct QueryRoot; + + #[graphql_object(scalar = MyScalarValue)] + impl QueryRoot { + fn human() -> Human { + Human { id: "human-32" } + } + } + + #[tokio::test] + async fn resolves_fields() { + const DOC: &str = r#"{ + human { + id + } + }"#; + + let schema = schema_with_scalar::(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"human": {"id": "human-32"}}), vec![])), + ); + } +} + +mod explicit_generic_scalar { + use std::marker::PhantomData; + + use super::*; + + #[derive(GraphQLObject)] + #[graphql(scalar = S)] + struct Human { + id: &'static str, + #[graphql(ignore)] + _scalar: PhantomData, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human<__S: Clone>() -> Human<__S> { + Human { + id: "human-32", + _scalar: PhantomData, + } + } + } + + #[tokio::test] + async fn resolves_fields() { + const DOC: &str = r#"{ + human { + id + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"human": {"id": "human-32"}}), vec![])), + ); + } +} + +mod bounded_generic_scalar { + use super::*; + + #[derive(GraphQLObject)] + #[graphql(scalar = S: ScalarValue + Clone)] + struct Human { + id: &'static str, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn human() -> Human { + Human { id: "human-32" } + } + } + + #[tokio::test] + async fn resolves_fields() { + const DOC: &str = r#"{ + human { + id + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"human": {"id": "human-32"}}), vec![])), + ); + } +} + +mod explicit_custom_context { + use super::*; + + struct CustomContext(String); + + impl juniper::Context for CustomContext {} + + #[derive(GraphQLObject)] + #[graphql(context = CustomContext)] + struct Human<'s> { + id: &'s str, + } + + struct QueryRoot; + + #[graphql_object(context = CustomContext)] + impl QueryRoot { + fn human(ctx: &CustomContext) -> Human<'_> { + Human { id: ctx.0.as_str() } + } + } + + #[tokio::test] + async fn resolves_fields() { + const DOC: &str = r#"{ + human { + id + } + }"#; + + let schema = schema(QueryRoot); + let ctx = CustomContext("ctx!".into()); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &ctx).await, + Ok((graphql_value!({"human": {"id": "ctx!"}}), vec![])), + ); + } +} diff --git a/integration_tests/juniper_tests/src/codegen/scalar_value_transparent.rs b/integration_tests/juniper_tests/src/codegen/scalar_value_transparent.rs index 087bc91c2..613d6394f 100644 --- a/integration_tests/juniper_tests/src/codegen/scalar_value_transparent.rs +++ b/integration_tests/juniper_tests/src/codegen/scalar_value_transparent.rs @@ -63,10 +63,7 @@ fn test_scalar_value_custom() { let mut registry: Registry = Registry::new(FnvHashMap::default()); let meta = CustomUserId::meta(&(), &mut registry); assert_eq!(meta.name(), Some("MyUserId")); - assert_eq!( - meta.description(), - Some(&"custom description...".to_string()) - ); + assert_eq!(meta.description(), Some("custom description...")); let input: InputValue = serde_json::from_value(serde_json::json!("userId1")).unwrap(); let output: CustomUserId = FromInputValue::from_input_value(&input).unwrap(); @@ -81,5 +78,5 @@ fn test_scalar_value_custom() { fn test_scalar_value_doc_comment() { let mut registry: Registry = Registry::new(FnvHashMap::default()); let meta = IdWithDocComment::meta(&(), &mut registry); - assert_eq!(meta.description(), Some(&"The doc comment...".to_string())); + assert_eq!(meta.description(), Some("The doc comment...")); } diff --git a/integration_tests/juniper_tests/src/codegen/subscription_attr.rs b/integration_tests/juniper_tests/src/codegen/subscription_attr.rs new file mode 100644 index 000000000..f2357ca1b --- /dev/null +++ b/integration_tests/juniper_tests/src/codegen/subscription_attr.rs @@ -0,0 +1,1822 @@ +//! Tests for `#[graphql_subscription]` macro. + +use std::pin::Pin; + +use futures::{future, stream, FutureExt as _, StreamExt as _}; +use juniper::{ + execute, graphql_object, graphql_subscription, graphql_value, resolve_into_stream, + DefaultScalarValue, EmptyMutation, ExecutionError, Executor, FieldError, FieldResult, + GraphQLError, GraphQLInputObject, GraphQLType, IntoFieldError, RootNode, ScalarValue, Value, + ValuesStream, Variables, +}; + +struct Query; + +#[graphql_object] +impl Query { + fn empty() -> bool { + true + } +} + +fn schema<'q, C, Qry, Sub>( + query_root: Qry, + subscription_root: Sub, +) -> RootNode<'q, Qry, EmptyMutation, Sub> +where + Qry: GraphQLType + 'q, + Sub: GraphQLType + 'q, +{ + RootNode::new(query_root, EmptyMutation::::new(), subscription_root) +} + +fn schema_with_scalar<'q, S, C, Qry, Sub>( + query_root: Qry, + subscription_root: Sub, +) -> RootNode<'q, Qry, EmptyMutation, Sub, S> +where + Qry: GraphQLType + 'q, + Sub: GraphQLType + 'q, + S: ScalarValue + 'q, +{ + RootNode::new_with_scalar_value(query_root, EmptyMutation::::new(), subscription_root) +} + +type Stream<'a, I> = Pin + Send + 'a>>; + +async fn extract_next<'a, S: ScalarValue>( + input: Result<(Value>, Vec>), GraphQLError<'a>>, +) -> Result<(Value, Vec>), GraphQLError<'a>> { + let (stream, errs) = input?; + if !errs.is_empty() { + return Ok((Value::Null, errs)); + } + + if let Value::Object(obj) = stream { + for (name, mut val) in obj { + if let Value::Scalar(ref mut stream) = val { + return match stream.next().await { + Some(Ok(val)) => Ok((graphql_value!({ name: val }), vec![])), + Some(Err(e)) => Ok((Value::Null, vec![e])), + None => Ok((Value::Null, vec![])), + }; + } + } + } + + panic!("Expected to get Value::Object containing a Stream") +} + +mod trivial { + use super::*; + + struct Human; + + #[graphql_subscription] + impl Human { + async fn id() -> Stream<'static, String> { + Box::pin(stream::once(future::ready("human-32".to_owned()))) + } + + // TODO: Make work for `Stream<'_, String>`. + async fn home_planet(&self) -> Stream<'static, String> { + Box::pin(stream::once(future::ready("earth".to_owned()))) + } + } + + #[tokio::test] + async fn resolves_id_field() { + const DOC: &str = r#"subscription { + id + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"id": "human-32"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_home_planet_field() { + const DOC: &str = r#"subscription { + homePlanet + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"homePlanet": "earth"}), vec![])), + ); + } + + #[tokio::test] + async fn is_graphql_object() { + const DOC: &str = r#"{ + __type(name: "Human") { + kind + } + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"kind": "OBJECT"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_name() { + const DOC: &str = r#"{ + __type(name: "Human") { + name + } + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "Human"}}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Human") { + description + } + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"description": None}}), vec![])), + ); + } +} + +mod raw_method { + use super::*; + + struct Human; + + #[graphql_subscription] + impl Human { + async fn r#my_id() -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("human-32"))) + } + + async fn r#async(&self) -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("async-32"))) + } + } + + #[tokio::test] + async fn resolves_my_id_field() { + const DOC: &str = r#"subscription { + myId + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"myId": "human-32"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_async_field() { + const DOC: &str = r#"subscription { + async + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"async": "async-32"}), vec![])), + ); + } + + #[tokio::test] + async fn has_correct_name() { + const DOC: &str = r#"{ + __type(name: "Human") { + name + kind + fields { + name + } + } + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": { + "name": "Human", + "kind": "OBJECT", + "fields": [{"name": "myId"}, {"name": "async"}], + }}), + vec![], + )), + ); + } +} + +mod ignored_method { + use super::*; + + struct Human; + + #[graphql_subscription] + impl Human { + async fn id() -> Stream<'static, String> { + Box::pin(stream::once(future::ready("human-32".to_owned()))) + } + + #[allow(dead_code)] + #[graphql(ignore)] + fn planet() -> &'static str { + "earth" + } + } + + #[tokio::test] + async fn resolves_id_field() { + const DOC: &str = r#"subscription { + id + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"id": "human-32"}), vec![])), + ); + } + + #[tokio::test] + async fn is_not_field() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields { + name + } + } + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [{"name": "id"}]}}), + vec![], + )), + ); + } +} + +mod fallible_method { + use super::*; + + struct CustomError; + + impl IntoFieldError for CustomError { + fn into_field_error(self) -> FieldError { + juniper::FieldError::new("Whatever", graphql_value!({"code": "some"})) + } + } + + struct Human; + + #[graphql_subscription] + impl Human { + async fn id(&self) -> Result, CustomError> { + Ok(Box::pin(stream::once(future::ready("human-32".to_owned())))) + } + + async fn home_planet<__S>() -> FieldResult, __S> { + Ok(Box::pin(stream::once(future::ready("earth")))) + } + } + + #[tokio::test] + async fn resolves_id_field() { + const DOC: &str = r#"subscription { + id + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"id": "human-32"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_home_planet_field() { + const DOC: &str = r#"subscription { + homePlanet + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"homePlanet": "earth"}), vec![])), + ); + } + + #[tokio::test] + async fn has_correct_graphql_type() { + const DOC: &str = r#"{ + __type(name: "Human") { + name + kind + fields { + name + type { + kind + ofType { + name + } + } + } + } + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": { + "name": "Human", + "kind": "OBJECT", + "fields": [{ + "name": "id", + "type": {"kind": "NON_NULL", "ofType": {"name": "String"}}, + }, { + "name": "homePlanet", + "type": {"kind": "NON_NULL", "ofType": {"name": "String"}}, + }] + }}), + vec![], + )), + ); + } +} + +mod argument { + use super::*; + + struct Human; + + #[graphql_subscription] + impl Human { + async fn id(arg: String) -> Stream<'static, String> { + Box::pin(stream::once(future::ready(arg))) + } + + async fn home_planet( + &self, + r#raw_arg: String, + r#async: Option, + ) -> Stream<'static, String> { + Box::pin(stream::once(future::ready(format!( + "{},{:?}", + r#raw_arg, r#async + )))) + } + } + + #[tokio::test] + async fn resolves_id_field() { + const DOC: &str = r#"subscription { + id(arg: "human-32") + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"id": "human-32"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_home_planet_field() { + const DOC: &str = r#"subscription { + homePlanet(rawArg: "earth") + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"homePlanet": "earth,None"}), vec![])), + ); + } + + #[tokio::test] + async fn has_correct_name() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields { + name + args { + name + } + } + } + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [ + {"name": "id", "args": [{"name": "arg"}]}, + {"name": "homePlanet", "args": [{"name": "rawArg"}, {"name": "async"}]}, + ]}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields { + args { + description + } + } + } + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [ + {"args": [{"description": None}]}, + {"args": [{"description": None}, {"description": None}]}, + ]}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn has_no_defaults() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields { + args { + defaultValue + } + } + } + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [ + {"args": [{"defaultValue": None}]}, + {"args": [{"defaultValue": None}, {"defaultValue": None}]}, + ]}}), + vec![], + )), + ); + } +} + +mod default_argument { + use super::*; + + #[derive(GraphQLInputObject, Debug)] + struct Point { + x: i32, + } + + struct Human; + + #[graphql_subscription] + impl Human { + async fn id( + &self, + #[graphql(default)] arg1: i32, + #[graphql(default = "second".to_string())] arg2: String, + #[graphql(default = true)] r#arg3: bool, + ) -> Stream<'static, String> { + Box::pin(stream::once(future::ready(format!( + "{}|{}&{}", + arg1, arg2, r#arg3 + )))) + } + + async fn info(#[graphql(default = Point { x: 1 })] coord: Point) -> Stream<'static, i32> { + Box::pin(stream::once(future::ready(coord.x))) + } + } + + #[tokio::test] + async fn resolves_id_field() { + let schema = schema(Query, Human); + + for (input, expected) in &[ + ("subscription { id }", "0|second&true"), + ("subscription { id(arg1: 1) }", "1|second&true"), + (r#"subscription { id(arg2: "") }"#, "0|&true"), + (r#"subscription { id(arg1: 2, arg2: "") }"#, "2|&true"), + ( + r#"subscription { id(arg1: 1, arg2: "", arg3: false) }"#, + "1|&false", + ), + ] { + let expected: &str = *expected; + + assert_eq!( + resolve_into_stream(*input, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({ "id": expected }), vec![])), + ); + } + } + + #[tokio::test] + async fn resolves_info_field() { + let schema = schema(Query, Human); + + for (input, expected) in &[ + ("subscription { info }", 1), + ("subscription { info(coord: { x: 2 }) }", 2), + ] { + let expected: i32 = *expected; + + assert_eq!( + resolve_into_stream(*input, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({ "info": expected }), vec![])), + ); + } + } + + #[tokio::test] + async fn has_defaults() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields { + args { + name + defaultValue + type { + name + ofType { + name + } + } + } + } + } + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [{ + "args": [{ + "name": "arg1", + "defaultValue": "0", + "type": {"name": "Int", "ofType": None}, + }, { + "name": "arg2", + "defaultValue": r#""second""#, + "type": {"name": "String", "ofType": None}, + }, { + "name": "arg3", + "defaultValue": "true", + "type": {"name": "Boolean", "ofType": None}, + }], + }, { + "args": [{ + "name": "coord", + "defaultValue": "{x: 1}", + "type": {"name": "Point", "ofType": None}, + }], + }]}}), + vec![], + )), + ); + } +} + +mod generic { + use super::*; + + struct Human { + id: A, + _home_planet: B, + } + + #[graphql_subscription] + impl Human { + async fn id(&self) -> Stream<'static, i32> { + Box::pin(stream::once(future::ready(self.id))) + } + } + + #[graphql_subscription(name = "HumanString")] + impl Human { + async fn id(&self) -> Stream<'static, String> { + Box::pin(stream::once(future::ready(self.id.to_owned()))) + } + } + + #[tokio::test] + async fn resolves_human() { + const DOC: &str = r#"subscription { + id + }"#; + + let schema = schema( + Query, + Human { + id: 34i32, + _home_planet: (), + }, + ); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"id": 34}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_human_string() { + const DOC: &str = r#"subscription { + id + }"#; + + let schema = schema( + Query, + Human { + id: "human-32".to_owned(), + _home_planet: (), + }, + ); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"id": "human-32"}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_name_without_type_params() { + const DOC: &str = r#"{ + __type(name: "Human") { + name + } + }"#; + + let schema = schema( + Query, + Human { + id: 0i32, + _home_planet: (), + }, + ); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "Human"}}), vec![])), + ); + } +} + +mod generic_lifetime { + use super::*; + + struct Human<'p, A = ()> { + id: A, + home_planet: &'p str, + } + + #[graphql_subscription] + impl<'p> Human<'p, i32> { + async fn id(&self) -> Stream<'static, i32> { + Box::pin(stream::once(future::ready(self.id))) + } + + // TODO: Make it work with `Stream<'_, &str>`. + async fn planet(&self) -> Stream<'static, String> { + Box::pin(stream::once(future::ready(self.home_planet.to_owned()))) + } + } + + #[graphql_subscription(name = "HumanString")] + impl<'id, 'p> Human<'p, &'id str> { + // TODO: Make it work with `Stream<'_, &str>`. + async fn id(&self) -> Stream<'static, String> { + Box::pin(stream::once(future::ready(self.id.to_owned()))) + } + + // TODO: Make it work with `Stream<'_, &str>`. + async fn planet(&self) -> Stream<'static, String> { + Box::pin(stream::once(future::ready(self.home_planet.to_owned()))) + } + } + + #[tokio::test] + async fn resolves_human_id_field() { + const DOC: &str = r#"subscription { + id + }"#; + + let schema = schema( + Query, + Human { + id: 34i32, + home_planet: "earth", + }, + ); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"id": 34}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_human_planet_field() { + const DOC: &str = r#"subscription { + planet + }"#; + + let schema = schema( + Query, + Human { + id: 34i32, + home_planet: "earth", + }, + ); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"planet": "earth"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_human_string_id_field() { + const DOC: &str = r#"subscription { + id + }"#; + + let schema = schema( + Query, + Human { + id: "human-32", + home_planet: "mars", + }, + ); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"id": "human-32"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_human_string_planet_field() { + const DOC: &str = r#"subscription { + planet + }"#; + + let schema = schema( + Query, + Human { + id: "human-32", + home_planet: "mars", + }, + ); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"planet": "mars"}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_name_without_type_params() { + const DOC: &str = r#"{ + __type(name: "Human") { + name + } + }"#; + + let schema = schema( + Query, + Human { + id: 34i32, + home_planet: "earth", + }, + ); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "Human"}}), vec![])), + ); + } +} + +mod description_from_doc_comment { + use super::*; + + struct Human; + + /// Rust docs. + #[graphql_subscription] + impl Human { + /// Rust `id` docs. + /// Here. + async fn id() -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("human-32"))) + } + } + + #[tokio::test] + async fn uses_doc_comment_as_description() { + const DOC: &str = r#"{ + __type(name: "Human") { + description + fields { + description + } + } + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": { + "description": "Rust docs.", + "fields": [{"description": "Rust `id` docs.\nHere."}], + }}), + vec![], + )), + ); + } +} + +mod deprecation_from_attr { + use super::*; + + struct Human; + + #[graphql_subscription] + impl Human { + async fn id() -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("human-32"))) + } + + #[deprecated] + async fn a(&self) -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("a"))) + } + + #[deprecated(note = "Use `id`.")] + async fn b(&self) -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("b"))) + } + } + + #[tokio::test] + async fn resolves_id_field() { + const DOC: &str = r#"subscription { + id + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"id": "human-32"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_deprecated_a_field() { + const DOC: &str = r#"subscription { + a + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"a": "a"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_deprecated_b_field() { + const DOC: &str = r#"subscription { + b + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"b": "b"}), vec![])), + ); + } + + #[tokio::test] + async fn deprecates_fields() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields(includeDeprecated: true) { + name + isDeprecated + } + } + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [ + {"name": "id", "isDeprecated": false}, + {"name": "a", "isDeprecated": true}, + {"name": "b", "isDeprecated": true}, + ]}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn provides_deprecation_reason() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields(includeDeprecated: true) { + name + deprecationReason + } + } + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [ + {"name": "id", "deprecationReason": None}, + {"name": "a", "deprecationReason": None}, + {"name": "b", "deprecationReason": "Use `id`."}, + ]}}), + vec![], + )), + ); + } +} + +mod explicit_name_description_and_deprecation { + use super::*; + + struct Human; + + /// Rust docs. + #[graphql_subscription(name = "MyHuman", desc = "My human.")] + impl Human { + /// Rust `id` docs. + #[graphql(name = "myId", desc = "My human ID.", deprecated = "Not used.")] + #[deprecated(note = "Should be omitted.")] + async fn id( + #[graphql(name = "myName", desc = "My argument.", default)] _n: String, + ) -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("human-32"))) + } + + #[graphql(deprecated)] + #[deprecated(note = "Should be omitted.")] + async fn a(&self) -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("a"))) + } + + async fn b(&self) -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("b"))) + } + } + + #[tokio::test] + async fn resolves_deprecated_id_field() { + const DOC: &str = r#"subscription { + myId + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"myId": "human-32"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_deprecated_a_field() { + const DOC: &str = r#"subscription { + a + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"a": "a"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_b_field() { + const DOC: &str = r#"subscription { + b + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"b": "b"}), vec![])), + ); + } + + #[tokio::test] + async fn uses_custom_name() { + const DOC: &str = r#"{ + __type(name: "MyHuman") { + name + fields(includeDeprecated: true) { + name + args { + name + } + } + } + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": { + "name": "MyHuman", + "fields": [ + {"name": "myId", "args": [{"name": "myName"}]}, + {"name": "a", "args": []}, + {"name": "b", "args": []}, + ], + }}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_custom_description() { + const DOC: &str = r#"{ + __type(name: "MyHuman") { + description + fields(includeDeprecated: true) { + name + description + args { + description + } + } + } + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": { + "description": "My human.", + "fields": [{ + "name": "myId", + "description": "My human ID.", + "args": [{"description": "My argument."}], + }, { + "name": "a", + "description": None, + "args": [], + }, { + "name": "b", + "description": None, + "args": [], + }], + }}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_custom_deprecation() { + const DOC: &str = r#"{ + __type(name: "MyHuman") { + fields(includeDeprecated: true) { + name + isDeprecated + deprecationReason + } + } + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": { + "fields": [{ + "name": "myId", + "isDeprecated": true, + "deprecationReason": "Not used.", + }, { + "name": "a", + "isDeprecated": true, + "deprecationReason": None, + }, { + "name": "b", + "isDeprecated": false, + "deprecationReason": None, + }], + }}), + vec![], + )), + ); + } +} + +mod renamed_all_fields_and_args { + use super::*; + + struct Human; + + #[graphql_subscription(rename_all = "none")] + impl Human { + async fn id() -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("human-32"))) + } + + async fn home_planet(&self, planet_name: String) -> Stream<'static, String> { + Box::pin(stream::once(future::ready(planet_name))) + } + + async fn r#async_info(r#my_num: i32) -> Stream<'static, i32> { + Box::pin(stream::once(future::ready(r#my_num))) + } + } + + #[tokio::test] + async fn resolves_id_field() { + const DOC: &str = r#"subscription { + id + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"id": "human-32"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_home_planet_field() { + const DOC: &str = r#"subscription { + home_planet(planet_name: "earth") + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"home_planet": "earth"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_async_info_field() { + const DOC: &str = r#"subscription { + async_info(my_num: 3) + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"async_info": 3}), vec![])), + ); + } + + #[tokio::test] + async fn uses_correct_fields_and_args_names() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields { + name + args { + name + } + } + } + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [ + {"name": "id", "args": []}, + {"name": "home_planet", "args": [{"name": "planet_name"}]}, + {"name": "async_info", "args": [{"name": "my_num"}]}, + ]}}), + vec![], + )), + ); + } +} + +mod explicit_scalar { + use super::*; + + struct Human; + + #[graphql_subscription(scalar = DefaultScalarValue)] + impl Human { + async fn id(&self) -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("human-32"))) + } + + async fn home_planet() -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("earth"))) + } + } + + #[tokio::test] + async fn resolves_id_field() { + const DOC: &str = r#"subscription { + id + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"id": "human-32"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_home_planet_field() { + const DOC: &str = r#"subscription { + homePlanet + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"homePlanet": "earth"}), vec![])), + ); + } +} + +mod custom_scalar { + use crate::custom_scalar::MyScalarValue; + + use super::*; + + struct Human; + + #[graphql_subscription(scalar = MyScalarValue)] + impl Human { + async fn id(&self) -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("human-32"))) + } + + async fn home_planet() -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("earth"))) + } + } + + #[tokio::test] + async fn resolves_id_field() { + const DOC: &str = r#"subscription { + id + }"#; + + let schema = schema_with_scalar::(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"id": "human-32"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_home_planet_field() { + const DOC: &str = r#"subscription { + homePlanet + }"#; + + let schema = schema_with_scalar::(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"homePlanet": "earth"}), vec![])), + ); + } +} + +mod explicit_generic_scalar { + use std::marker::PhantomData; + + use super::*; + + struct Human(PhantomData); + + #[graphql_subscription(scalar = S)] + impl Human { + async fn id(&self) -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("human-32"))) + } + + async fn home_planet(_executor: &Executor<'_, '_, (), S>) -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("earth"))) + } + } + + #[tokio::test] + async fn resolves_id_field() { + const DOC: &str = r#"subscription { + id + }"#; + + let schema = schema(Query, Human::(PhantomData)); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"id": "human-32"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_home_planet_field() { + const DOC: &str = r#"subscription { + homePlanet + }"#; + + let schema = schema(Query, Human::(PhantomData)); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"homePlanet": "earth"}), vec![])), + ); + } +} + +mod bounded_generic_scalar { + use super::*; + + struct Human; + + #[graphql_subscription(scalar = S: ScalarValue + Clone)] + impl Human { + async fn id(&self) -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("human-32"))) + } + + async fn home_planet( + _executor: &Executor<'_, '_, (), S>, + ) -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("earth"))) + } + } + + #[tokio::test] + async fn resolves_id_field() { + const DOC: &str = r#"subscription { + id + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"id": "human-32"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_home_planet_field() { + const DOC: &str = r#"subscription { + homePlanet + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"homePlanet": "earth"}), vec![])), + ); + } +} + +mod explicit_custom_context { + use super::*; + + struct CustomContext(String); + + impl juniper::Context for CustomContext {} + + struct QueryRoot; + + #[graphql_object(context = CustomContext)] + impl QueryRoot { + fn empty() -> bool { + true + } + } + + struct Human; + + #[graphql_subscription(context = CustomContext)] + impl Human { + // TODO: Make work for `Stream<'c, String>`. + async fn id<'c>(&self, context: &'c CustomContext) -> Stream<'static, String> { + Box::pin(stream::once(future::ready(context.0.clone()))) + } + + // TODO: Make work for `Stream<'_, String>`. + async fn info(_ctx: &()) -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("human being"))) + } + + async fn more(#[graphql(context)] custom: &CustomContext) -> Stream<'static, String> { + Box::pin(stream::once(future::ready(custom.0.clone()))) + } + } + + #[tokio::test] + async fn resolves_id_field() { + const DOC: &str = r#"subscription { + id + }"#; + + let schema = schema(QueryRoot, Human); + let ctx = CustomContext("ctx!".into()); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &ctx) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"id": "ctx!"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_info_field() { + const DOC: &str = r#"subscription { + info + }"#; + + let schema = schema(QueryRoot, Human); + let ctx = CustomContext("ctx!".into()); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &ctx) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"info": "human being"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_more_field() { + const DOC: &str = r#"subscription { + more + }"#; + + let schema = schema(QueryRoot, Human); + let ctx = CustomContext("ctx!".into()); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &ctx) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"more": "ctx!"}), vec![])), + ); + } +} + +mod inferred_custom_context_from_field { + use super::*; + + struct CustomContext(String); + + impl juniper::Context for CustomContext {} + + struct QueryRoot; + + #[graphql_object(context = CustomContext)] + impl QueryRoot { + fn empty() -> bool { + true + } + } + + struct Human; + + #[graphql_subscription] + impl Human { + // TODO: Make work for `Stream<'c, String>`. + async fn id<'c>(&self, context: &'c CustomContext) -> Stream<'static, String> { + Box::pin(stream::once(future::ready(context.0.clone()))) + } + + // TODO: Make work for `Stream<'_, String>`. + async fn info(_ctx: &()) -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("human being"))) + } + + async fn more(#[graphql(context)] custom: &CustomContext) -> Stream<'static, String> { + Box::pin(stream::once(future::ready(custom.0.clone()))) + } + } + + #[tokio::test] + async fn resolves_id_field() { + const DOC: &str = r#"subscription { + id + }"#; + + let schema = schema(QueryRoot, Human); + let ctx = CustomContext("ctx!".into()); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &ctx) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"id": "ctx!"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_info_field() { + const DOC: &str = r#"subscription { + info + }"#; + + let schema = schema(QueryRoot, Human); + let ctx = CustomContext("ctx!".into()); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &ctx) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"info": "human being"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_more_field() { + const DOC: &str = r#"subscription { + more + }"#; + + let schema = schema(QueryRoot, Human); + let ctx = CustomContext("ctx!".into()); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &ctx) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"more": "ctx!"}), vec![])), + ); + } +} + +mod executor { + use juniper::LookAheadMethods as _; + + use super::*; + + struct Human; + + #[graphql_subscription(scalar = S: ScalarValue)] + impl Human { + // TODO: Make work for `Stream<'e, &'e str>`. + async fn id<'e, S>(&self, executor: &'e Executor<'_, '_, (), S>) -> Stream<'static, String> + where + S: ScalarValue, + { + Box::pin(stream::once(future::ready( + executor.look_ahead().field_name().to_owned(), + ))) + } + + async fn info( + &self, + arg: String, + #[graphql(executor)] _another: &Executor<'_, '_, (), S>, + ) -> Stream<'static, String> { + Box::pin(stream::once(future::ready(arg))) + } + + // TODO: Make work for `Stream<'e, &'e str>`. + async fn info2<'e, S>( + _executor: &'e Executor<'_, '_, (), S>, + ) -> Stream<'static, &'static str> { + Box::pin(stream::once(future::ready("no info"))) + } + } + + #[tokio::test] + async fn resolves_id_field() { + const DOC: &str = r#"subscription { + id + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"id": "id"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_info_field() { + const DOC: &str = r#"subscription { + info(arg: "input!") + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"info": "input!"}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_info2_field() { + const DOC: &str = r#"subscription { + info2 + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + resolve_into_stream(DOC, None, &schema, &Variables::new(), &()) + .then(|s| extract_next(s)) + .await, + Ok((graphql_value!({"info2": "no info"}), vec![])), + ); + } + + #[tokio::test] + async fn not_arg() { + const DOC: &str = r#"{ + __type(name: "Human") { + fields { + name + args { + name + } + } + } + }"#; + + let schema = schema(Query, Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [ + {"name": "id", "args": []}, + {"name": "info", "args": [{"name": "arg"}]}, + {"name": "info2", "args": []}, + ]}}), + vec![], + )), + ); + } +} diff --git a/integration_tests/juniper_tests/src/codegen/union_attr.rs b/integration_tests/juniper_tests/src/codegen/union_attr.rs index 52820fb6a..4d831ba9c 100644 --- a/integration_tests/juniper_tests/src/codegen/union_attr.rs +++ b/integration_tests/juniper_tests/src/codegen/union_attr.rs @@ -660,6 +660,184 @@ mod custom_scalar { } } +mod explicit_generic_scalar { + use super::*; + + #[graphql_union(scalar = S)] + trait Character { + fn as_human(&self) -> Option<&Human> { + None + } + fn as_droid(&self) -> Option<&Droid> { + None + } + } + + impl Character for Human { + fn as_human(&self) -> Option<&Human> { + Some(&self) + } + } + + impl Character for Droid { + fn as_droid(&self) -> Option<&Droid> { + Some(&self) + } + } + + type DynCharacter<'a, S> = dyn Character + Send + Sync + 'a; + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + impl QueryRoot { + fn character<__S: ScalarValue>(&self) -> Box> { + let ch: Box> = match self { + Self::Human => Box::new(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Box::new(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + }; + ch + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot::Droid); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } +} + +mod bounded_generic_scalar { + use super::*; + + #[graphql_union(scalar = S: ScalarValue + Clone)] + trait Character { + fn as_human(&self) -> Option<&Human> { + None + } + fn as_droid(&self) -> Option<&Droid> { + None + } + } + + impl Character for Human { + fn as_human(&self) -> Option<&Human> { + Some(&self) + } + } + + impl Character for Droid { + fn as_droid(&self) -> Option<&Droid> { + Some(&self) + } + } + + type DynCharacter<'a> = dyn Character + Send + Sync + 'a; + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Box> { + let ch: Box> = match self { + Self::Human => Box::new(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Box::new(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + }; + ch + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot::Droid); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } +} + mod explicit_custom_context { use super::*; diff --git a/integration_tests/juniper_tests/src/codegen/union_derive.rs b/integration_tests/juniper_tests/src/codegen/union_derive.rs index ed90741bd..6fd203649 100644 --- a/integration_tests/juniper_tests/src/codegen/union_derive.rs +++ b/integration_tests/juniper_tests/src/codegen/union_derive.rs @@ -284,6 +284,101 @@ mod generic_enum { } } +/* TODO: make it work +mod generic_lifetime_enum { + use super::*; + + #[derive(GraphQLObject)] + struct LifetimeHuman<'id> { + id: &'id str, + } + + #[derive(GraphQLObject)] + struct GenericDroid { + id: String, + #[graphql(ignore)] + _t: PhantomData, + } + + #[derive(GraphQLUnion)] + enum Character<'id, B = ()> { + A(LifetimeHuman<'id>), + B(GenericDroid), + } + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Character<'_> { + match self { + Self::Human => Character::A(LifetimeHuman { id: "human-32" }), + Self::Droid => Character::B(GenericDroid { + id: "droid-99".to_string(), + _t: PhantomData, + }), + } + } + } + + const DOC: &str = r#"{ + character { + ... on LifetimeHuman { + humanId: id + } + ... on GenericDroid { + droidId: id + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot::Droid); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_type_name_without_type_params() { + const DOC: &str = r#"{ + __type(name: "Character") { + name + } + }"#; + + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "Character"}}), vec![])), + ); + } +} +*/ + mod description_from_doc_comments { use super::*; @@ -571,6 +666,150 @@ mod custom_scalar { } } +mod explicit_generic_scalar { + use super::*; + + #[derive(GraphQLUnion)] + #[graphql(scalar = S)] + enum Character { + A(Human), + B(Droid), + #[graphql(ignore)] + _P(PhantomData), + } + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + impl QueryRoot { + fn character<__S: ScalarValue>(&self) -> Character<__S> { + match self { + Self::Human => Character::A(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Character::B(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + } + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot::Droid); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } +} + +mod bounded_generic_scalar { + use super::*; + + #[derive(GraphQLUnion)] + #[graphql(scalar = S: ScalarValue + Clone)] + enum Character { + A(Human), + B(Droid), + } + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Character { + match self { + Self::Human => Character::A(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Character::B(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + } + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot::Droid); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } +} + mod custom_context { use super::*; @@ -1124,8 +1363,8 @@ mod trivial_struct { #[derive(GraphQLUnion)] #[graphql(context = Database)] #[graphql( - on Human = Character::as_human, - on Droid = Character::as_droid, + on Human = Character::as_human, + on Droid = Character::as_droid, )] struct Character { id: String, @@ -1227,6 +1466,75 @@ mod trivial_struct { )), ); } + + #[tokio::test] + async fn is_graphql_union() { + const DOC: &str = r#"{ + __type(name: "Character") { + kind + } + }"#; + + let schema = schema(QueryRoot::Human); + let db = Database { + human: Some(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + droid: None, + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok((graphql_value!({"__type": {"kind": "UNION"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_name() { + const DOC: &str = r#"{ + __type(name: "Character") { + name + } + }"#; + + let schema = schema(QueryRoot::Human); + let db = Database { + human: Some(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + droid: None, + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok((graphql_value!({"__type": {"name": "Character"}}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Character") { + description + } + }"#; + + let schema = schema(QueryRoot::Human); + let db = Database { + human: Some(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + droid: None, + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok((graphql_value!({"__type": {"description": None}}), vec![])), + ); + } } mod generic_struct { @@ -1473,6 +1781,8 @@ mod full_featured_struct { } } +/// Checks that union with boxed variants resolves okay. +/// See [#845](https://github.com/graphql-rust/juniper/issues/845) for details. mod issue_845 { use std::sync::Arc; diff --git a/integration_tests/juniper_tests/src/explicit_null.rs b/integration_tests/juniper_tests/src/explicit_null.rs index 3ade4b528..0349f2621 100644 --- a/integration_tests/juniper_tests/src/explicit_null.rs +++ b/integration_tests/juniper_tests/src/explicit_null.rs @@ -1,4 +1,7 @@ -use juniper::*; +use juniper::{ + graphql_object, graphql_value, EmptyMutation, EmptySubscription, GraphQLInputObject, + InputValue, Nullable, +}; pub struct Context; @@ -6,12 +9,12 @@ impl juniper::Context for Context {} pub struct Query; -#[derive(juniper::GraphQLInputObject)] +#[derive(GraphQLInputObject)] struct ObjectInput { field: Nullable, } -#[graphql_object(Context=Context)] +#[graphql_object(context = Context)] impl Query { fn is_explicit_null(arg: Nullable) -> bool { arg.is_explicit_null() @@ -26,40 +29,38 @@ type Schema = juniper::RootNode<'static, Query, EmptyMutation, EmptySub #[tokio::test] async fn explicit_null() { - let ctx = Context; - let query = r#" - query Foo($emptyObj: ObjectInput!, $literalNullObj: ObjectInput!) { - literalOneIsExplicitNull: isExplicitNull(arg: 1) - literalNullIsExplicitNull: isExplicitNull(arg: null) - noArgIsExplicitNull: isExplicitNull - literalOneFieldIsExplicitNull: objectFieldIsExplicitNull(obj: {field: 1}) - literalNullFieldIsExplicitNull: objectFieldIsExplicitNull(obj: {field: null}) - noFieldIsExplicitNull: objectFieldIsExplicitNull(obj: {}) - emptyVariableObjectFieldIsExplicitNull: objectFieldIsExplicitNull(obj: $emptyObj) - literalNullVariableObjectFieldIsExplicitNull: objectFieldIsExplicitNull(obj: $literalNullObj) - } + query Foo($emptyObj: ObjectInput!, $literalNullObj: ObjectInput!) { + literalOneIsExplicitNull: isExplicitNull(arg: 1) + literalNullIsExplicitNull: isExplicitNull(arg: null) + noArgIsExplicitNull: isExplicitNull + literalOneFieldIsExplicitNull: objectFieldIsExplicitNull(obj: {field: 1}) + literalNullFieldIsExplicitNull: objectFieldIsExplicitNull(obj: {field: null}) + noFieldIsExplicitNull: objectFieldIsExplicitNull(obj: {}) + emptyVariableObjectFieldIsExplicitNull: objectFieldIsExplicitNull(obj: $emptyObj) + literalNullVariableObjectFieldIsExplicitNull: objectFieldIsExplicitNull(obj: $literalNullObj) + } "#; + let schema = &Schema::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + ); + let vars = [ + ("emptyObj".to_string(), InputValue::Object(vec![])), + ( + "literalNullObj".to_string(), + InputValue::object(vec![("field", InputValue::null())].into_iter().collect()), + ), + ]; + let (data, errors) = juniper::execute( query, None, - &Schema::new( - Query, - EmptyMutation::::new(), - EmptySubscription::::new(), - ), - &[ - ("emptyObj".to_string(), InputValue::Object(vec![])), - ( - "literalNullObj".to_string(), - InputValue::object(vec![("field", InputValue::null())].into_iter().collect()), - ), - ] - .iter() - .cloned() - .collect(), - &ctx, + &schema, + &vars.iter().cloned().collect(), + &Context, ) .await .unwrap(); diff --git a/integration_tests/juniper_tests/src/issue_371.rs b/integration_tests/juniper_tests/src/issue_371.rs index d4085e60d..571a1587e 100644 --- a/integration_tests/juniper_tests/src/issue_371.rs +++ b/integration_tests/juniper_tests/src/issue_371.rs @@ -1,7 +1,12 @@ -// Original author of this test is . +//! Checks that `executor.look_ahead().field_name()` is correct in presence of +//! multiple query fields. +//! See [#371](https://github.com/graphql-rust/juniper/issues/371) for details. +//! +//! Original author of this test is [@davidpdrsn](https://github.com/davidpdrsn). use juniper::{ - graphql_object, EmptyMutation, EmptySubscription, LookAheadMethods as _, RootNode, Variables, + graphql_object, EmptyMutation, EmptySubscription, Executor, LookAheadMethods as _, RootNode, + ScalarValue, Variables, }; pub struct Context; @@ -12,14 +17,14 @@ pub struct Query; #[graphql_object(context = Context)] impl Query { - fn users(exec: &Executor) -> Vec { - let lh = exec.look_ahead(); + fn users<__S: ScalarValue>(executor: &Executor<'_, '_, Context, __S>) -> Vec { + let lh = executor.look_ahead(); assert_eq!(lh.field_name(), "users"); vec![User] } - fn countries(exec: &Executor) -> Vec { - let lh = exec.look_ahead(); + fn countries<__S: ScalarValue>(executor: &Executor<'_, '_, Context, __S>) -> Vec { + let lh = executor.look_ahead(); assert_eq!(lh.field_name(), "countries"); vec![Country] } @@ -49,98 +54,54 @@ type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription #[tokio::test] async fn users() { - let ctx = Context; - - let query = r#"{ users { id } }"#; - - let (_, errors) = juniper::execute( - query, - None, - &Schema::new( - Query, - EmptyMutation::::new(), - EmptySubscription::::new(), - ), - &juniper::Variables::new(), - &ctx, - ) - .await - .unwrap(); + let query = "{ users { id } }"; + + let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()); + let (_, errors) = juniper::execute(query, None, &schema, &Variables::new(), &Context) + .await + .unwrap(); assert_eq!(errors.len(), 0); } #[tokio::test] async fn countries() { - let ctx = Context; - - let query = r#"{ countries { id } }"#; + let query = "{ countries { id } }"; - let (_, errors) = juniper::execute( - query, - None, - &Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()), - &juniper::Variables::new(), - &ctx, - ) - .await - .unwrap(); + let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()); + let (_, errors) = juniper::execute(query, None, &schema, &Variables::new(), &Context) + .await + .unwrap(); assert_eq!(errors.len(), 0); } #[tokio::test] async fn both() { - let ctx = Context; - - let query = r#" - { + let query = "{ countries { id } users { id } - } - "#; - - let (_, errors) = juniper::execute( - query, - None, - &Schema::new( - Query, - EmptyMutation::::new(), - EmptySubscription::::new(), - ), - &Variables::new(), - &ctx, - ) - .await - .unwrap(); + }"; + + let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()); + let (_, errors) = juniper::execute(query, None, &schema, &Variables::new(), &Context) + .await + .unwrap(); assert_eq!(errors.len(), 0); } #[tokio::test] async fn both_in_different_order() { - let ctx = Context; - - let query = r#" - { + let query = "{ users { id } countries { id } - } - "#; - - let (_, errors) = juniper::execute( - query, - None, - &Schema::new( - Query, - EmptyMutation::::new(), - EmptySubscription::::new(), - ), - &Variables::new(), - &ctx, - ) - .await - .unwrap(); + }"; + + let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()); + let (_, errors) = juniper::execute(query, None, &schema, &Variables::new(), &Context) + .await + .unwrap(); assert_eq!(errors.len(), 0); } diff --git a/integration_tests/juniper_tests/src/issue_372.rs b/integration_tests/juniper_tests/src/issue_372.rs index a78cf0d1a..661c90db6 100644 --- a/integration_tests/juniper_tests/src/issue_372.rs +++ b/integration_tests/juniper_tests/src/issue_372.rs @@ -1,3 +1,6 @@ +//! Checks that `__typename` field queries okay on root types. +//! See [#372](https://github.com/graphql-rust/juniper/issues/372) for details. + use futures::{stream, StreamExt as _}; use juniper::{graphql_object, graphql_subscription, graphql_value, RootNode, Value, Variables}; diff --git a/integration_tests/juniper_tests/src/issue_398.rs b/integration_tests/juniper_tests/src/issue_398.rs index 5a42ac238..0d5557411 100644 --- a/integration_tests/juniper_tests/src/issue_398.rs +++ b/integration_tests/juniper_tests/src/issue_398.rs @@ -1,13 +1,18 @@ -// Original author of this test is . +//! Checks that `executor.look_ahead()` on a fragment with nested type works okay. +//! See [#398](https://github.com/graphql-rust/juniper/issues/398) for details. +//! +//! Original author of this test is [@davidpdrsn](https://github.com/davidpdrsn). -use juniper::{graphql_object, EmptyMutation, EmptySubscription, RootNode, Variables}; +use juniper::{ + graphql_object, EmptyMutation, EmptySubscription, Executor, RootNode, ScalarValue, Variables, +}; struct Query; #[graphql_object] impl Query { - fn users(executor: &Executor) -> Vec { - // This doesn't cause a panic + fn users(executor: &Executor<'_, '_, (), S>) -> Vec { + // This doesn't cause a panic. executor.look_ahead(); vec![User { @@ -22,7 +27,7 @@ struct User { #[graphql_object] impl User { - fn country(&self, executor: &Executor) -> &Country { + fn country(&self, executor: &Executor<'_, '_, (), S>) -> &Country { // This panics! executor.look_ahead(); @@ -44,7 +49,7 @@ impl Country { type Schema = RootNode<'static, Query, EmptyMutation<()>, EmptySubscription<()>>; #[tokio::test] -async fn test_lookahead_from_fragment_with_nested_type() { +async fn lookahead_from_fragment_with_nested_type() { let _ = juniper::execute( r#" query Query { diff --git a/integration_tests/juniper_tests/src/issue_407.rs b/integration_tests/juniper_tests/src/issue_407.rs index 6b4e79fb2..11c6075c9 100644 --- a/integration_tests/juniper_tests/src/issue_407.rs +++ b/integration_tests/juniper_tests/src/issue_407.rs @@ -1,4 +1,9 @@ -use juniper::*; +//! Checks that using a fragment of an implementation in an interface works okay. +//! See [#407](https://github.com/graphql-rust/juniper/issues/407) for details. + +use juniper::{ + graphql_interface, graphql_object, EmptyMutation, EmptySubscription, GraphQLObject, Variables, +}; struct Query; @@ -53,7 +58,7 @@ impl Query { type Schema = juniper::RootNode<'static, Query, EmptyMutation, EmptySubscription>; #[tokio::test] -async fn test_fragments_in_interface() { +async fn fragments_in_interface() { let query = r#" query Query { characters { @@ -71,30 +76,19 @@ async fn test_fragments_in_interface() { } "#; - let (_, errors) = juniper::execute( - query, - None, - &Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()), - &Variables::new(), - &(), - ) - .await - .unwrap(); + let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + let (_, errors) = juniper::execute(query, None, &schema, &Variables::new(), &()) + .await + .unwrap(); assert_eq!(errors.len(), 0); - let (_, errors) = juniper::execute_sync( - query, - None, - &Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()), - &Variables::new(), - &(), - ) - .unwrap(); + let (_, errors) = juniper::execute_sync(query, None, &schema, &Variables::new(), &()).unwrap(); assert_eq!(errors.len(), 0); } #[tokio::test] -async fn test_inline_fragments_in_interface() { +async fn inline_fragments_in_interface() { let query = r#" query Query { characters { @@ -116,24 +110,13 @@ async fn test_inline_fragments_in_interface() { } "#; - let (_, errors) = juniper::execute( - query, - None, - &Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()), - &Variables::new(), - &(), - ) - .await - .unwrap(); + let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + let (_, errors) = juniper::execute(query, None, &schema, &Variables::new(), &()) + .await + .unwrap(); assert_eq!(errors.len(), 0); - let (_, errors) = juniper::execute_sync( - query, - None, - &Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()), - &Variables::new(), - &(), - ) - .unwrap(); + let (_, errors) = juniper::execute_sync(query, None, &schema, &Variables::new(), &()).unwrap(); assert_eq!(errors.len(), 0); } diff --git a/integration_tests/juniper_tests/src/issue_500.rs b/integration_tests/juniper_tests/src/issue_500.rs index 7dbb595fd..221b1720e 100644 --- a/integration_tests/juniper_tests/src/issue_500.rs +++ b/integration_tests/juniper_tests/src/issue_500.rs @@ -1,10 +1,13 @@ -use juniper::*; +//! Checks that using nested fragments works okay. +//! See [#500](https://github.com/graphql-rust/juniper/issues/500) for details. + +use juniper::{graphql_object, EmptyMutation, EmptySubscription, Executor, ScalarValue, Variables}; struct Query; -#[juniper::graphql_object] +#[graphql_object] impl Query { - fn users(executor: &Executor) -> Vec { + fn users(executor: &Executor<'_, '_, (), S>) -> Vec { executor.look_ahead(); vec![User { @@ -19,9 +22,9 @@ struct User { city: City, } -#[juniper::graphql_object] +#[graphql_object] impl User { - fn city(&self, executor: &Executor) -> &City { + fn city(&self, executor: &Executor<'_, '_, (), S>) -> &City { executor.look_ahead(); &self.city } @@ -31,9 +34,9 @@ struct City { country: Country, } -#[juniper::graphql_object] +#[graphql_object] impl City { - fn country(&self, executor: &Executor) -> &Country { + fn country(&self, executor: &Executor<'_, '_, (), S>) -> &Country { executor.look_ahead(); &self.country } @@ -43,7 +46,7 @@ struct Country { id: i32, } -#[juniper::graphql_object] +#[graphql_object] impl Country { fn id(&self) -> i32 { self.id @@ -53,7 +56,7 @@ impl Country { type Schema = juniper::RootNode<'static, Query, EmptyMutation<()>, EmptySubscription<()>>; #[tokio::test] -async fn test_nested_fragments() { +async fn nested_fragments() { let query = r#" query Query { users { @@ -78,15 +81,10 @@ async fn test_nested_fragments() { } "#; - let (_, errors) = juniper::execute( - query, - None, - &Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()), - &Variables::new(), - &(), - ) - .await - .unwrap(); + let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()); + let (_, errors) = juniper::execute(query, None, &schema, &Variables::new(), &()) + .await + .unwrap(); assert_eq!(errors.len(), 0); } diff --git a/integration_tests/juniper_tests/src/issue_798.rs b/integration_tests/juniper_tests/src/issue_798.rs index f3ec4d700..a8f7ec0de 100644 --- a/integration_tests/juniper_tests/src/issue_798.rs +++ b/integration_tests/juniper_tests/src/issue_798.rs @@ -1,3 +1,6 @@ +//! Checks that interface field resolves okay on a union. +//! See [#798](https://github.com/graphql-rust/juniper/issues/798) for details. + use juniper::{ graphql_interface, graphql_object, graphql_value, EmptyMutation, EmptySubscription, GraphQLObject, GraphQLUnion, RootNode, Variables, @@ -64,10 +67,10 @@ impl Query { } } -type Schema = RootNode<'static, Query, EmptyMutation<()>, EmptySubscription<()>>; +type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>; #[tokio::test] -async fn test_interface_inline_fragment_on_union() { +async fn interface_inline_fragment_on_union() { let query = r#" query Query { field { @@ -85,15 +88,10 @@ async fn test_interface_inline_fragment_on_union() { } "#; - let (res, errors) = juniper::execute( - query, - None, - &Schema::new(Query::Human, EmptyMutation::new(), EmptySubscription::new()), - &Variables::new(), - &(), - ) - .await - .unwrap(); + let schema = Schema::new(Query::Human, EmptyMutation::new(), EmptySubscription::new()); + let (res, errors) = juniper::execute(query, None, &schema, &Variables::new(), &()) + .await + .unwrap(); assert_eq!(errors.len(), 0); assert_eq!( @@ -107,14 +105,9 @@ async fn test_interface_inline_fragment_on_union() { }), ); - let (res, errors) = juniper::execute_sync( - query, - None, - &Schema::new(Query::Droid, EmptyMutation::new(), EmptySubscription::new()), - &Variables::new(), - &(), - ) - .unwrap(); + let schema = Schema::new(Query::Droid, EmptyMutation::new(), EmptySubscription::new()); + let (res, errors) = + juniper::execute_sync(query, None, &schema, &Variables::new(), &()).unwrap(); assert_eq!(errors.len(), 0); assert_eq!( @@ -130,7 +123,7 @@ async fn test_interface_inline_fragment_on_union() { } #[tokio::test] -async fn test_interface_fragment_on_union() { +async fn interface_fragment_on_union() { let query = r#" query Query { field { @@ -150,15 +143,10 @@ async fn test_interface_fragment_on_union() { } "#; - let (res, errors) = juniper::execute( - query, - None, - &Schema::new(Query::Human, EmptyMutation::new(), EmptySubscription::new()), - &Variables::new(), - &(), - ) - .await - .unwrap(); + let schema = Schema::new(Query::Human, EmptyMutation::new(), EmptySubscription::new()); + let (res, errors) = juniper::execute(query, None, &schema, &Variables::new(), &()) + .await + .unwrap(); assert_eq!(errors.len(), 0); assert_eq!( @@ -172,14 +160,9 @@ async fn test_interface_fragment_on_union() { }), ); - let (res, errors) = juniper::execute_sync( - query, - None, - &Schema::new(Query::Droid, EmptyMutation::new(), EmptySubscription::new()), - &Variables::new(), - &(), - ) - .unwrap(); + let schema = Schema::new(Query::Droid, EmptyMutation::new(), EmptySubscription::new()); + let (res, errors) = + juniper::execute_sync(query, None, &schema, &Variables::new(), &()).unwrap(); assert_eq!(errors.len(), 0); assert_eq!( diff --git a/integration_tests/juniper_tests/src/issue_914.rs b/integration_tests/juniper_tests/src/issue_914.rs index be15514fb..aa9b86c38 100644 --- a/integration_tests/juniper_tests/src/issue_914.rs +++ b/integration_tests/juniper_tests/src/issue_914.rs @@ -1,4 +1,7 @@ -use juniper::*; +//! Checks that multiple fragments on sub types don't override each other. +//! See [#914](https://github.com/graphql-rust/juniper/issues/914) for details. + +use juniper::{graphql_object, EmptyMutation, EmptySubscription, GraphQLObject, Variables}; struct Query; @@ -32,7 +35,7 @@ impl Query { type Schema = juniper::RootNode<'static, Query, EmptyMutation, EmptySubscription>; #[tokio::test] -async fn test_fragments_with_nested_objects_dont_override_previous_selections() { +async fn fragments_with_nested_objects_dont_override_previous_selections() { let query = r#" query Query { foo { @@ -72,25 +75,15 @@ async fn test_fragments_with_nested_objects_dont_override_previous_selections() } "#; - let (async_value, errors) = juniper::execute( - query, - None, - &Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()), - &Variables::new(), - &(), - ) - .await - .unwrap(); + let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + let (async_value, errors) = juniper::execute(query, None, &schema, &Variables::new(), &()) + .await + .unwrap(); assert_eq!(errors.len(), 0); - let (sync_value, errors) = juniper::execute_sync( - query, - None, - &Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()), - &Variables::new(), - &(), - ) - .unwrap(); + let (sync_value, errors) = + juniper::execute_sync(query, None, &schema, &Variables::new(), &()).unwrap(); assert_eq!(errors.len(), 0); assert_eq!(async_value, sync_value); diff --git a/integration_tests/juniper_tests/src/issue_922.rs b/integration_tests/juniper_tests/src/issue_922.rs index 35de87faf..b99803fb3 100644 --- a/integration_tests/juniper_tests/src/issue_922.rs +++ b/integration_tests/juniper_tests/src/issue_922.rs @@ -1,8 +1,14 @@ -use juniper::*; +//! Checks that fields on interface fragment spreads resolve okay. +//! See [#922](https://github.com/graphql-rust/juniper/issues/922) for details. + +use juniper::{ + graphql_interface, graphql_object, graphql_value, EmptyMutation, EmptySubscription, + GraphQLObject, Variables, +}; struct Query; -#[juniper::graphql_object] +#[graphql_object] impl Query { fn characters() -> Vec { vec![ @@ -18,21 +24,21 @@ impl Query { } } -#[juniper::graphql_interface(for = [Human, Droid])] +#[graphql_interface(for = [Human, Droid])] trait Character { fn id(&self) -> i32; fn name(&self) -> String; } -#[derive(juniper::GraphQLObject)] +#[derive(GraphQLObject)] #[graphql(impl = CharacterValue)] struct Human { pub id: i32, pub name: String, } -#[juniper::graphql_interface] +#[graphql_interface] impl Character for Human { fn id(&self) -> i32 { self.id @@ -43,14 +49,14 @@ impl Character for Human { } } -#[derive(juniper::GraphQLObject)] +#[derive(GraphQLObject)] #[graphql(impl = CharacterValue)] struct Droid { pub id: i32, pub name: String, } -#[juniper::graphql_interface] +#[graphql_interface] impl Character for Droid { fn id(&self) -> i32 { self.id @@ -61,10 +67,10 @@ impl Character for Droid { } } -type Schema = juniper::RootNode<'static, Query, EmptyMutation<()>, EmptySubscription<()>>; +type Schema = juniper::RootNode<'static, Query, EmptyMutation, EmptySubscription>; #[tokio::test] -async fn test_fragment_on_interface() { +async fn fragment_on_interface() { let query = r#" query Query { characters { @@ -85,15 +91,11 @@ async fn test_fragment_on_interface() { } "#; - let (res, errors) = juniper::execute( - query, - None, - &Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()), - &Variables::new(), - &(), - ) - .await - .unwrap(); + let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + let (res, errors) = juniper::execute(query, None, &schema, &Variables::new(), &()) + .await + .unwrap(); assert_eq!(errors.len(), 0); assert_eq!( @@ -106,14 +108,8 @@ async fn test_fragment_on_interface() { }), ); - let (res, errors) = juniper::execute_sync( - query, - None, - &Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()), - &Variables::new(), - &(), - ) - .unwrap(); + let (res, errors) = + juniper::execute_sync(query, None, &schema, &Variables::new(), &()).unwrap(); assert_eq!(errors.len(), 0); assert_eq!( diff --git a/integration_tests/juniper_tests/src/issue_925.rs b/integration_tests/juniper_tests/src/issue_925.rs index 45180262e..435cb2a7b 100644 --- a/integration_tests/juniper_tests/src/issue_925.rs +++ b/integration_tests/juniper_tests/src/issue_925.rs @@ -1,8 +1,14 @@ -use juniper::*; +//! Checks that `FieldError` doesn't lose its extensions while being implicitly +//! converted from user defined subscription errors. +//! See [#925](https://github.com/graphql-rust/juniper/issues/925) for details. use futures::stream::BoxStream; +use juniper::{ + graphql_object, graphql_subscription, graphql_value, EmptyMutation, FieldError, GraphQLObject, + IntoFieldError, Object, ScalarValue, Value, Variables, +}; -#[derive(juniper::GraphQLObject)] +#[derive(GraphQLObject)] struct User { name: String, } @@ -33,17 +39,17 @@ fn users_stream() -> Result, Error> { struct Query; -#[juniper::graphql_object] +#[graphql_object] impl Query { fn users() -> Vec { vec![] } } -type Schema = juniper::RootNode<'static, Query, EmptyMutation<()>, SubscriptionsRoot>; +type Schema = juniper::RootNode<'static, Query, EmptyMutation, SubscriptionsRoot>; #[tokio::test] -async fn test_error_extensions() { +async fn error_extensions() { let sub = r#" subscription Users { users { @@ -52,18 +58,13 @@ async fn test_error_extensions() { } "#; - let (_, errors) = juniper::resolve_into_stream( - sub, - None, - &Schema::new(Query, EmptyMutation::new(), SubscriptionsRoot), - &Variables::new(), - &(), - ) - .await - .unwrap(); + let schema = Schema::new(Query, EmptyMutation::new(), SubscriptionsRoot); + let (_, errors) = juniper::resolve_into_stream(sub, None, &schema, &Variables::new(), &()) + .await + .unwrap(); assert_eq!( errors.first().unwrap().error().extensions(), - &graphql_value!({ "a": 42 }) + &graphql_value!({ "a": 42 }), ); } diff --git a/integration_tests/juniper_tests/src/issue_945.rs b/integration_tests/juniper_tests/src/issue_945.rs index 763fcd69a..9d8c0db85 100644 --- a/integration_tests/juniper_tests/src/issue_945.rs +++ b/integration_tests/juniper_tests/src/issue_945.rs @@ -1,4 +1,10 @@ -use juniper::*; +//! Checks that spreading untyped union fragment work okay. +//! See [#945](https://github.com/graphql-rust/juniper/issues/945) for details. + +use juniper::{ + graphql_object, graphql_value, EmptyMutation, EmptySubscription, GraphQLObject, GraphQLUnion, + Variables, +}; struct Query; @@ -34,10 +40,10 @@ struct Droid { pub sensor_color: String, } -type Schema = RootNode<'static, Query, EmptyMutation<()>, EmptySubscription<()>>; +type Schema = juniper::RootNode<'static, Query, EmptyMutation, EmptySubscription>; #[tokio::test] -async fn test_fragment_on_interface() { +async fn fragment_on_union() { let query = r#" query Query { artoo { @@ -58,38 +64,28 @@ async fn test_fragment_on_interface() { } "#; - let (res, errors) = execute( - query, - None, - &Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()), - &Variables::new(), - &(), - ) - .await - .unwrap(); + let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()); + + let (res, errors) = juniper::execute(query, None, &schema, &Variables::new(), &()) + .await + .unwrap(); assert_eq!(errors.len(), 0); assert_eq!( res, graphql_value!({ - "artoo": {"__typename": "Droid", "id": 1, "sensorColor": "red"} + "artoo": {"__typename": "Droid", "id": 1, "sensorColor": "red"}, }), ); - let (res, errors) = execute_sync( - query, - None, - &Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()), - &Variables::new(), - &(), - ) - .unwrap(); + let (res, errors) = + juniper::execute_sync(query, None, &schema, &Variables::new(), &()).unwrap(); assert_eq!(errors.len(), 0); assert_eq!( res, graphql_value!({ - "artoo": {"__typename": "Droid", "id": 1, "sensorColor": "red"} + "artoo": {"__typename": "Droid", "id": 1, "sensorColor": "red"}, }), ); } diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index dcbc294d2..eb0829b90 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -1,8 +1,19 @@ # master -- Allow spreading interface fragments on unions and other interfaces ([#965](https://github.com/graphql-rust/juniper/pull/965), [#798](https://github.com/graphql-rust/juniper/issues/798)) -- Expose `GraphQLRequest` fields ([#750](https://github.com/graphql-rust/juniper/issues/750)) -- Support using Rust array as GraphQL list ([#966](https://github.com/graphql-rust/juniper/pull/966), [#918](https://github.com/graphql-rust/juniper/issues/918)) +## Breaking Changes + +- `#[graphql_object]` and `#[graphql_subscription]` macros expansion now preserves defined `impl` blocks "as is" and reuses defined methods in opaque way. ([#971](https://github.com/graphql-rust/juniper/pull/971) +- `rename = ""` attribute's argument renamed to `rename_all = ""`. ([#971](https://github.com/graphql-rust/juniper/pull/971) + +## Features + +- Support using Rust array as GraphQL list. ([#966](https://github.com/graphql-rust/juniper/pull/966), [#918](https://github.com/graphql-rust/juniper/issues/918)) +- Expose `GraphQLRequest` fields. ([#750](https://github.com/graphql-rust/juniper/issues/750)) +- `#[graphql_interface]` macro now supports `rename_all = ""` argument influencing its fields and their arguments. ([#971](https://github.com/graphql-rust/juniper/pull/971) + +## Fixes + +- Allow spreading interface fragments on unions and other interfaces. ([#965](https://github.com/graphql-rust/juniper/pull/965), [#798](https://github.com/graphql-rust/juniper/issues/798)) # [[0.15.7] 2021-07-08](https://github.com/graphql-rust/juniper/releases/tag/juniper-v0.15.7) @@ -41,8 +52,8 @@ ## Features - Added async support. ([#2](https://github.com/graphql-rust/juniper/issues/2)) - - `execute()` is now async. Synchronous execution can still be used via `execute_sync()`. - - Field resolvers may optionally be declared as `async` and return a future. + - `execute()` is now async. Synchronous execution can still be used via `execute_sync()`. + - Field resolvers may optionally be declared as `async` and return a future. - Added *experimental* support for GraphQL subscriptions. ([#433](https://github.com/graphql-rust/juniper/pull/433)) diff --git a/juniper/src/ast.rs b/juniper/src/ast.rs index 07aed950b..06ac627d3 100644 --- a/juniper/src/ast.rs +++ b/juniper/src/ast.rs @@ -160,12 +160,13 @@ pub trait FromInputValue: Sized { /// Performs the conversion. fn from_input_value(v: &InputValue) -> Option; - /// Performs the conversion from an absent value (e.g. to distinguish between implicit and - /// explicit null). The default implementation just uses `from_input_value` as if an explicit - /// null were provided. This conversion must not fail. - fn from_implicit_null() -> Self { + /// Performs the conversion from an absent value (e.g. to distinguish between + /// implicit and explicit null). The default implementation just uses + /// `from_input_value` as if an explicit null were provided. + /// + /// This conversion must not fail. + fn from_implicit_null() -> Option { Self::from_input_value(&InputValue::::Null) - .expect("input value conversion from null must not fail") } } diff --git a/juniper/src/executor_tests/directives.rs b/juniper/src/executor_tests/directives.rs index 791fe2c68..50cc5a0ea 100644 --- a/juniper/src/executor_tests/directives.rs +++ b/juniper/src/executor_tests/directives.rs @@ -9,11 +9,11 @@ struct TestType; #[crate::graphql_object] impl TestType { - fn a() -> &str { + fn a() -> &'static str { "a" } - fn b() -> &str { + fn b() -> &'static str { "b" } } diff --git a/juniper/src/executor_tests/executor.rs b/juniper/src/executor_tests/executor.rs index fb45ba175..3d6f098dd 100644 --- a/juniper/src/executor_tests/executor.rs +++ b/juniper/src/executor_tests/executor.rs @@ -1,9 +1,9 @@ mod field_execution { use crate::{ ast::InputValue, + graphql_value, schema::model::RootNode, types::scalars::{EmptyMutation, EmptySubscription}, - value::Value, }; struct DataType; @@ -11,22 +11,22 @@ mod field_execution { #[crate::graphql_object] impl DataType { - fn a() -> &str { + fn a() -> &'static str { "Apple" } - fn b() -> &str { + fn b() -> &'static str { "Banana" } - fn c() -> &str { + fn c() -> &'static str { "Cookie" } - fn d() -> &str { + fn d() -> &'static str { "Donut" } - fn e() -> &str { + fn e() -> &'static str { "Egg" } - fn f() -> &str { + fn f() -> &'static str { "Fish" } @@ -41,13 +41,13 @@ mod field_execution { #[crate::graphql_object] impl DeepDataType { - fn a() -> &str { + fn a() -> &'static str { "Already Been Done" } - fn b() -> &str { + fn b() -> &'static str { "Boring" } - fn c() -> Vec> { + fn c() -> Vec> { vec![Some("Contrived"), None, Some("Confusing")] } @@ -64,30 +64,31 @@ mod field_execution { EmptySubscription::<()>::new(), ); let doc = r" - query Example($size: Int) { - a, - b, - x: c - ...c - f - ...on DataType { - pic(size: $size) - } - deep { - a - b - c - deeper { - a - b - } + query Example($size: Int) { + a, + b, + x: c + ...c + f + ...on DataType { + pic(size: $size) + } + deep { + a + b + c + deeper { + a + b + } + } } - } - fragment c on DataType { - d - e - }"; + fragment c on DataType { + d + e + } + "; let vars = vec![("size".to_owned(), InputValue::scalar(100))] .into_iter() @@ -103,60 +104,31 @@ mod field_execution { assert_eq!( result, - Value::object( - vec![ - ("a", Value::scalar("Apple")), - ("b", Value::scalar("Banana")), - ("x", Value::scalar("Cookie")), - ("d", Value::scalar("Donut")), - ("e", Value::scalar("Egg")), - ("f", Value::scalar("Fish")), - ("pic", Value::scalar("Pic of size: 100")), - ( - "deep", - Value::object( - vec![ - ("a", Value::scalar("Already Been Done")), - ("b", Value::scalar("Boring")), - ( - "c", - Value::list(vec![ - Value::scalar("Contrived"), - Value::null(), - Value::scalar("Confusing"), - ]), - ), - ( - "deeper", - Value::list(vec![ - Value::object( - vec![ - ("a", Value::scalar("Apple")), - ("b", Value::scalar("Banana")), - ] - .into_iter() - .collect(), - ), - Value::null(), - Value::object( - vec![ - ("a", Value::scalar("Apple")), - ("b", Value::scalar("Banana")), - ] - .into_iter() - .collect(), - ), - ]), - ), - ] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect() - ) + graphql_value!({ + "a": "Apple", + "b": "Banana", + "x": "Cookie", + "d": "Donut", + "e": "Egg", + "f": "Fish", + "pic": "Pic of size: 100", + "deep": { + "a": "Already Been Done", + "b": "Boring", + "c": ["Contrived", None, "Confusing"], + "deeper": [ + { + "a": "Apple", + "b": "Banana", + }, + None, + { + "a": "Apple", + "b": "Banana", + }, + ], + }, + }), ); } } @@ -165,20 +137,19 @@ mod merge_parallel_fragments { use crate::{ schema::model::RootNode, types::scalars::{EmptyMutation, EmptySubscription}, - value::Value, }; struct Type; #[crate::graphql_object] impl Type { - fn a() -> &str { + fn a() -> &'static str { "Apple" } - fn b() -> &str { + fn b() -> &'static str { "Banana" } - fn c() -> &str { + fn c() -> &'static str { "Cherry" } fn deep() -> Type { @@ -194,15 +165,16 @@ mod merge_parallel_fragments { EmptySubscription::<()>::new(), ); let doc = r" - { a, ...FragOne, ...FragTwo } - fragment FragOne on Type { - b - deep { b, deeper: deep { b } } - } - fragment FragTwo on Type { - c - deep { c, deeper: deep { c } } - }"; + { a, ...FragOne, ...FragTwo } + fragment FragOne on Type { + b + deep { b, deeper: deep { b } } + } + fragment FragTwo on Type { + c + deep { c, deeper: deep { c } } + } + "; let vars = vec![].into_iter().collect(); @@ -216,37 +188,19 @@ mod merge_parallel_fragments { assert_eq!( result, - Value::object( - vec![ - ("a", Value::scalar("Apple")), - ("b", Value::scalar("Banana")), - ( - "deep", - Value::object( - vec![ - ("b", Value::scalar("Banana")), - ( - "deeper", - Value::object( - vec![ - ("b", Value::scalar("Banana")), - ("c", Value::scalar("Cherry")), - ] - .into_iter() - .collect(), - ), - ), - ("c", Value::scalar("Cherry")), - ] - .into_iter() - .collect(), - ), - ), - ("c", Value::scalar("Cherry")), - ] - .into_iter() - .collect() - ) + graphql_value!({ + "a": "Apple", + "b": "Banana", + "deep": { + "b": "Banana", + "deeper": { + "b": "Banana", + "c": "Cherry", + }, + "c": "Cherry", + }, + "c": "Cherry", + }), ); } } @@ -255,7 +209,6 @@ mod merge_parallel_inline_fragments { use crate::{ schema::model::RootNode, types::scalars::{EmptyMutation, EmptySubscription}, - value::Value, }; struct Type; @@ -263,13 +216,13 @@ mod merge_parallel_inline_fragments { #[crate::graphql_object] impl Type { - fn a() -> &str { + fn a() -> &'static str { "Apple" } - fn b() -> &str { + fn b() -> &'static str { "Banana" } - fn c() -> &str { + fn c() -> &'static str { "Cherry" } fn deep() -> Type { @@ -282,13 +235,13 @@ mod merge_parallel_inline_fragments { #[crate::graphql_object] impl Other { - fn a() -> &str { + fn a() -> &'static str { "Apple" } - fn b() -> &str { + fn b() -> &'static str { "Banana" } - fn c() -> &str { + fn c() -> &'static str { "Cherry" } fn deep() -> Type { @@ -307,29 +260,29 @@ mod merge_parallel_inline_fragments { EmptySubscription::<()>::new(), ); let doc = r" - { a, ...FragOne } - fragment FragOne on Type { - b - deep: deep { + { a, ...FragOne } + fragment FragOne on Type { b - deeper: other { - deepest: deep { - b - } - } - - ... on Type { - c + deep: deep { + b deeper: other { deepest: deep { - c + b + } + } + + ... on Type { + c + deeper: other { + deepest: deep { + c + } } } } + c } - - c - }"; + "; let vars = vec![].into_iter().collect(); @@ -343,61 +296,26 @@ mod merge_parallel_inline_fragments { assert_eq!( result, - Value::object( - vec![ - ("a", Value::scalar("Apple")), - ("b", Value::scalar("Banana")), - ( - "deep", - Value::object( - vec![ - ("b", Value::scalar("Banana")), - ( - "deeper", - Value::list(vec![ - Value::object( - vec![( - "deepest", - Value::object( - vec![ - ("b", Value::scalar("Banana")), - ("c", Value::scalar("Cherry")), - ] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect(), - ), - Value::object( - vec![( - "deepest", - Value::object( - vec![ - ("b", Value::scalar("Banana")), - ("c", Value::scalar("Cherry")), - ] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect(), - ), - ]), - ), - ("c", Value::scalar("Cherry")), - ] - .into_iter() - .collect(), - ), - ), - ("c", Value::scalar("Cherry")), - ] - .into_iter() - .collect() - ) + graphql_value!({ + "a": "Apple", + "b": "Banana", + "deep": { + "b": "Banana", + "deeper": [{ + "deepest": { + "b": "Banana", + "c": "Cherry", + }, + }, { + "deepest": { + "b": "Banana", + "c": "Cherry", + }, + }], + "c": "Cherry", + }, + "c": "Cherry", + }), ); } } @@ -405,9 +323,9 @@ mod merge_parallel_inline_fragments { mod threads_context_correctly { use crate::{ executor::Context, + graphql_value, schema::model::RootNode, types::scalars::{EmptyMutation, EmptySubscription}, - value::Value, }; struct Schema; @@ -454,14 +372,7 @@ mod threads_context_correctly { println!("Result: {:#?}", result); - assert_eq!( - result, - Value::object( - vec![("a", Value::scalar("Context value"))] - .into_iter() - .collect() - ) - ); + assert_eq!(result, graphql_value!({"a": "Context value"})); } } @@ -475,6 +386,7 @@ mod dynamic_context_switching { schema::model::RootNode, types::scalars::{EmptyMutation, EmptySubscription}, value::Value, + Executor, ScalarValue, }; struct Schema; @@ -494,7 +406,11 @@ mod dynamic_context_switching { #[graphql_object(context = OuterContext)] impl Schema { - fn item_opt(_context: &OuterContext, key: i32) -> Option<(&InnerContext, ItemRef)> { + fn item_opt<'e, S: ScalarValue>( + executor: &'e Executor<'_, '_, OuterContext, S>, + _context: &OuterContext, + key: i32, + ) -> Option<(&'e InnerContext, ItemRef)> { executor.context().items.get(&key).map(|c| (c, ItemRef)) } @@ -653,11 +569,9 @@ mod dynamic_context_switching { EmptyMutation::::new(), EmptySubscription::::new(), ); - let doc = r" - { + let doc = r"{ missing: itemRes(key: 2) { value } - } - "; + }"; let vars = vec![].into_iter().collect(); @@ -687,7 +601,7 @@ mod dynamic_context_switching { assert_eq!( errs, vec![ExecutionError::new( - SourcePosition::new(25, 2, 12), + SourcePosition::new(14, 1, 12), &["missing"], FieldError::new("Could not find key 2", Value::null()), )] @@ -880,13 +794,13 @@ mod propagates_errors_to_nullable_fields { fn non_nullable_field() -> Inner { Inner } - fn nullable_error_field() -> FieldResult> { + fn nullable_error_field() -> FieldResult> { Err("Error for nullableErrorField")? } - fn non_nullable_error_field() -> FieldResult<&str> { + fn non_nullable_error_field() -> FieldResult<&'static str> { Err("Error for nonNullableErrorField")? } - fn custom_error_field() -> Result<&str, CustomError> { + fn custom_error_field() -> Result<&'static str, CustomError> { Err(CustomError::NotFound) } } @@ -1160,18 +1074,18 @@ mod propagates_errors_to_nullable_fields { mod named_operations { use crate::{ + graphql_object, graphql_value, schema::model::RootNode, types::scalars::{EmptyMutation, EmptySubscription}, - value::Value, GraphQLError, }; struct Schema; - #[crate::graphql_object] + #[graphql_object] impl Schema { - fn a(p: Option) -> &str { - let _ = p; + fn a(p: Option) -> &'static str { + drop(p); "b" } } @@ -1187,16 +1101,12 @@ mod named_operations { let vars = vec![].into_iter().collect(); - let (result, errs) = crate::execute(doc, None, &schema, &vars, &()) + let (res, errs) = crate::execute(doc, None, &schema, &vars, &()) .await .expect("Execution failed"); assert_eq!(errs, []); - - assert_eq!( - result, - Value::object(vec![("a", Value::scalar("b"))].into_iter().collect()) - ); + assert_eq!(res, graphql_value!({"a": "b"})); } #[tokio::test] @@ -1210,16 +1120,12 @@ mod named_operations { let vars = vec![].into_iter().collect(); - let (result, errs) = crate::execute(doc, None, &schema, &vars, &()) + let (res, errs) = crate::execute(doc, None, &schema, &vars, &()) .await .expect("Execution failed"); assert_eq!(errs, []); - - assert_eq!( - result, - Value::object(vec![("a", Value::scalar("b"))].into_iter().collect()) - ); + assert_eq!(res, graphql_value!({"a": "b"})); } #[tokio::test] @@ -1234,16 +1140,12 @@ mod named_operations { let vars = vec![].into_iter().collect(); - let (result, errs) = crate::execute(doc, Some("OtherExample"), &schema, &vars, &()) + let (res, errs) = crate::execute(doc, Some("OtherExample"), &schema, &vars, &()) .await .expect("Execution failed"); assert_eq!(errs, []); - - assert_eq!( - result, - Value::object(vec![("second", Value::scalar("b"))].into_iter().collect()) - ); + assert_eq!(res, graphql_value!({"second": "b"})); } #[tokio::test] diff --git a/juniper/src/executor_tests/introspection/input_object.rs b/juniper/src/executor_tests/introspection/input_object.rs index 8e86553e8..d9c48bda5 100644 --- a/juniper/src/executor_tests/introspection/input_object.rs +++ b/juniper/src/executor_tests/introspection/input_object.rs @@ -3,6 +3,7 @@ use crate::{ ast::{FromInputValue, InputValue}, executor::Variables, + graphql_object, graphql_value, schema::model::RootNode, types::scalars::{EmptyMutation, EmptySubscription}, value::{DefaultScalarValue, Object, Value}, @@ -82,7 +83,7 @@ struct FieldWithDefaults { field_two: i32, } -#[crate::graphql_object] +#[graphql_object] impl Root { fn test_field( a1: DefaultName, @@ -149,8 +150,7 @@ where #[tokio::test] async fn default_name_introspection() { - let doc = r#" - { + let doc = r#"{ __type(name: "DefaultName") { name description @@ -165,70 +165,35 @@ async fn default_name_introspection() { defaultValue } } - } - "#; + }"#; run_type_info_query(doc, |type_info, fields| { assert_eq!( type_info.get_field_value("name"), - Some(&Value::scalar("DefaultName")) + Some(&graphql_value!("DefaultName")), ); assert_eq!( type_info.get_field_value("description"), - Some(&Value::null()) + Some(&graphql_value!(None)), ); assert_eq!(fields.len(), 2); - - assert!(fields.contains(&Value::object( - vec![ - ("name", Value::scalar("fieldOne")), - ("description", Value::null()), - ( - "type", - Value::object( - vec![( - "ofType", - Value::object( - vec![("name", Value::scalar("String"))] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect(), - ), - ), - ("defaultValue", Value::null()), - ] - .into_iter() - .collect(), - ))); - - assert!(fields.contains(&Value::object( - vec![ - ("name", Value::scalar("fieldTwo")), - ("description", Value::null()), - ( - "type", - Value::object( - vec![( - "ofType", - Value::object( - vec![("name", Value::scalar("String"))] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect(), - ), - ), - ("defaultValue", Value::null()), - ] - .into_iter() - .collect(), - ))); + assert!(fields.contains(&graphql_value!({ + "name": "fieldOne", + "description": None, + "type": { + "ofType": {"name": "String"}, + }, + "defaultValue": None, + }))); + assert!(fields.contains(&graphql_value!({ + "name": "fieldTwo", + "description": None, + "type": { + "ofType": {"name": "String"}, + }, + "defaultValue": None, + }))); }) .await; } @@ -256,8 +221,7 @@ fn default_name_input_value() { #[tokio::test] async fn no_trailing_comma_introspection() { - let doc = r#" - { + let doc = r#"{ __type(name: "NoTrailingComma") { name description @@ -272,78 +236,42 @@ async fn no_trailing_comma_introspection() { defaultValue } } - } - "#; + }"#; run_type_info_query(doc, |type_info, fields| { assert_eq!( type_info.get_field_value("name"), - Some(&Value::scalar("NoTrailingComma")) + Some(&graphql_value!("NoTrailingComma")), ); assert_eq!( type_info.get_field_value("description"), - Some(&Value::null()) + Some(&graphql_value!(None)), ); assert_eq!(fields.len(), 2); - - assert!(fields.contains(&Value::object( - vec![ - ("name", Value::scalar("fieldOne")), - ("description", Value::null()), - ( - "type", - Value::object( - vec![( - "ofType", - Value::object( - vec![("name", Value::scalar("String"))] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect(), - ), - ), - ("defaultValue", Value::null()), - ] - .into_iter() - .collect(), - ))); - - assert!(fields.contains(&Value::object( - vec![ - ("name", Value::scalar("fieldTwo")), - ("description", Value::null()), - ( - "type", - Value::object( - vec![( - "ofType", - Value::object( - vec![("name", Value::scalar("String"))] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect(), - ), - ), - ("defaultValue", Value::null()), - ] - .into_iter() - .collect(), - ))); + assert!(fields.contains(&graphql_value!({ + "name": "fieldOne", + "description": None, + "type": { + "ofType": {"name": "String"}, + }, + "defaultValue": None, + }))); + assert!(fields.contains(&graphql_value!({ + "name": "fieldTwo", + "description": None, + "type": { + "ofType": {"name": "String"}, + }, + "defaultValue": None, + }))); }) .await; } #[tokio::test] async fn derive_introspection() { - let doc = r#" - { + let doc = r#"{ __type(name: "Derive") { name description @@ -358,45 +286,27 @@ async fn derive_introspection() { defaultValue } } - } - "#; + }"#; run_type_info_query(doc, |type_info, fields| { assert_eq!( type_info.get_field_value("name"), - Some(&Value::scalar("Derive")) + Some(&graphql_value!("Derive")), ); assert_eq!( type_info.get_field_value("description"), - Some(&Value::null()) + Some(&graphql_value!(None)), ); assert_eq!(fields.len(), 1); - - assert!(fields.contains(&Value::object( - vec![ - ("name", Value::scalar("fieldOne")), - ("description", Value::null()), - ( - "type", - Value::object( - vec![( - "ofType", - Value::object( - vec![("name", Value::scalar("String"))] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect(), - ), - ), - ("defaultValue", Value::null()), - ] - .into_iter() - .collect(), - ))); + assert!(fields.contains(&graphql_value!({ + "name": "fieldOne", + "description": None, + "type": { + "ofType": {"name": "String"}, + }, + "defaultValue": None, + }))); }) .await; } @@ -408,7 +318,7 @@ fn derive_derived() { "{:?}", Derive { field_one: "test".to_owned(), - } + }, ), "Derive { field_one: \"test\" }" ); @@ -416,8 +326,7 @@ fn derive_derived() { #[tokio::test] async fn named_introspection() { - let doc = r#" - { + let doc = r#"{ __type(name: "ANamedInputObject") { name description @@ -432,53 +341,34 @@ async fn named_introspection() { defaultValue } } - } - "#; + }"#; run_type_info_query(doc, |type_info, fields| { assert_eq!( type_info.get_field_value("name"), - Some(&Value::scalar("ANamedInputObject")) + Some(&graphql_value!("ANamedInputObject")) ); assert_eq!( type_info.get_field_value("description"), - Some(&Value::null()) + Some(&graphql_value!(None)) ); assert_eq!(fields.len(), 1); - - assert!(fields.contains(&Value::object( - vec![ - ("name", Value::scalar("fieldOne")), - ("description", Value::null()), - ( - "type", - Value::object( - vec![( - "ofType", - Value::object( - vec![("name", Value::scalar("String"))] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect(), - ), - ), - ("defaultValue", Value::null()), - ] - .into_iter() - .collect(), - ))); + assert!(fields.contains(&graphql_value!({ + "name": "fieldOne", + "description": None, + "type": { + "ofType": {"name": "String"}, + }, + "defaultValue": None, + }))); }) .await; } #[tokio::test] async fn description_introspection() { - let doc = r#" - { + let doc = r#"{ __type(name: "Description") { name description @@ -493,53 +383,34 @@ async fn description_introspection() { defaultValue } } - } - "#; + }"#; run_type_info_query(doc, |type_info, fields| { assert_eq!( type_info.get_field_value("name"), - Some(&Value::scalar("Description")) + Some(&graphql_value!("Description")), ); assert_eq!( type_info.get_field_value("description"), - Some(&Value::scalar("Description for the input object")) + Some(&graphql_value!("Description for the input object")), ); assert_eq!(fields.len(), 1); - - assert!(fields.contains(&Value::object( - vec![ - ("name", Value::scalar("fieldOne")), - ("description", Value::null()), - ( - "type", - Value::object( - vec![( - "ofType", - Value::object( - vec![("name", Value::scalar("String"))] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect(), - ), - ), - ("defaultValue", Value::null()), - ] - .into_iter() - .collect(), - ))); + assert!(fields.contains(&graphql_value!({ + "name": "fieldOne", + "description": None, + "type": { + "ofType": {"name": "String"}, + }, + "defaultValue": None, + }))); }) .await; } #[tokio::test] async fn field_description_introspection() { - let doc = r#" - { + let doc = r#"{ __type(name: "FieldDescription") { name description @@ -554,78 +425,42 @@ async fn field_description_introspection() { defaultValue } } - } - "#; + }"#; run_type_info_query(doc, |type_info, fields| { assert_eq!( type_info.get_field_value("name"), - Some(&Value::scalar("FieldDescription")) + Some(&graphql_value!("FieldDescription")), ); assert_eq!( type_info.get_field_value("description"), - Some(&Value::null()) + Some(&graphql_value!(None)), ); assert_eq!(fields.len(), 2); - - assert!(fields.contains(&Value::object( - vec![ - ("name", Value::scalar("fieldOne")), - ("description", Value::scalar("The first field")), - ( - "type", - Value::object( - vec![( - "ofType", - Value::object( - vec![("name", Value::scalar("String"))] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect(), - ), - ), - ("defaultValue", Value::null()), - ] - .into_iter() - .collect(), - ))); - - assert!(fields.contains(&Value::object( - vec![ - ("name", Value::scalar("fieldTwo")), - ("description", Value::scalar("The second field")), - ( - "type", - Value::object( - vec![( - "ofType", - Value::object( - vec![("name", Value::scalar("String"))] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect(), - ), - ), - ("defaultValue", Value::null()), - ] - .into_iter() - .collect(), - ))); + assert!(fields.contains(&graphql_value!({ + "name": "fieldOne", + "description": "The first field", + "type": { + "ofType": {"name": "String"}, + }, + "defaultValue": None, + }))); + assert!(fields.contains(&graphql_value!({ + "name": "fieldTwo", + "description": "The second field", + "type": { + "ofType": {"name": "String"}, + }, + "defaultValue": None, + }))); }) .await; } #[tokio::test] async fn field_with_defaults_introspection() { - let doc = r#" - { + let doc = r#"{ __type(name: "FieldWithDefaults") { name inputFields { @@ -636,42 +471,25 @@ async fn field_with_defaults_introspection() { defaultValue } } - } - "#; + }"#; run_type_info_query(doc, |type_info, fields| { assert_eq!( type_info.get_field_value("name"), - Some(&Value::scalar("FieldWithDefaults")) + Some(&graphql_value!("FieldWithDefaults")), ); assert_eq!(fields.len(), 2); - - assert!(fields.contains(&Value::object( - vec![ - ("name", Value::scalar("fieldOne")), - ( - "type", - Value::object(vec![("name", Value::scalar("Int"))].into_iter().collect()), - ), - ("defaultValue", Value::scalar("123")), - ] - .into_iter() - .collect(), - ))); - - assert!(fields.contains(&Value::object( - vec![ - ("name", Value::scalar("fieldTwo")), - ( - "type", - Value::object(vec![("name", Value::scalar("Int"))].into_iter().collect()), - ), - ("defaultValue", Value::scalar("456")), - ] - .into_iter() - .collect(), - ))); + assert!(fields.contains(&graphql_value!({ + "name": "fieldOne", + "type": {"name": "Int"}, + "defaultValue": "123", + }))); + assert!(fields.contains(&graphql_value!({ + "name": "fieldTwo", + "type": {"name": "Int"}, + "defaultValue": "456", + }))); }) .await; } diff --git a/juniper/src/executor_tests/introspection/mod.rs b/juniper/src/executor_tests/introspection/mod.rs index 74403ff85..8bf6a14e6 100644 --- a/juniper/src/executor_tests/introspection/mod.rs +++ b/juniper/src/executor_tests/introspection/mod.rs @@ -56,13 +56,11 @@ impl Root { Sample::One } - #[graphql(arguments( - first(description = "The first number",), - second(description = "The second number", default = 123), - ))] - /// A sample scalar field on the object - fn sample_scalar(first: i32, second: i32) -> Scalar { + fn sample_scalar( + #[graphql(description = "The first number")] first: i32, + #[graphql(description = "The second number", default = 123)] second: i32, + ) -> Scalar { Scalar(first + second) } } diff --git a/juniper/src/executor_tests/variables.rs b/juniper/src/executor_tests/variables.rs index 76f73e014..1cefcb83f 100644 --- a/juniper/src/executor_tests/variables.rs +++ b/juniper/src/executor_tests/variables.rs @@ -77,14 +77,9 @@ impl TestType { format!("{:?}", input) } - #[graphql( - arguments( - input( - default = "Hello World".to_string(), - ) - ) - )] - fn field_with_default_argument_value(input: String) -> String { + fn field_with_default_argument_value( + #[graphql(default = "Hello World")] input: String, + ) -> String { format!("{:?}", input) } diff --git a/juniper/src/integrations/bson.rs b/juniper/src/integrations/bson.rs index 15d66c949..95d83382c 100644 --- a/juniper/src/integrations/bson.rs +++ b/juniper/src/integrations/bson.rs @@ -1,3 +1,5 @@ +//! GraphQL support for [bson](https://github.com/mongodb/bson-rust) types. + use bson::{oid::ObjectId, DateTime as UtcDateTime}; use chrono::prelude::*; diff --git a/juniper/src/integrations/chrono.rs b/juniper/src/integrations/chrono.rs index 03be9dda7..56613fdd6 100644 --- a/juniper/src/integrations/chrono.rs +++ b/juniper/src/integrations/chrono.rs @@ -253,72 +253,68 @@ mod integration_test { use crate::{ executor::Variables, + graphql_object, graphql_value, schema::model::RootNode, types::scalars::{EmptyMutation, EmptySubscription}, - value::Value, }; #[tokio::test] async fn test_serialization() { struct Root; - #[crate::graphql_object] + #[graphql_object] #[cfg(feature = "scalar-naivetime")] impl Root { - fn exampleNaiveDate() -> NaiveDate { + fn example_naive_date() -> NaiveDate { NaiveDate::from_ymd(2015, 3, 14) } - fn exampleNaiveDateTime() -> NaiveDateTime { + fn example_naive_date_time() -> NaiveDateTime { NaiveDate::from_ymd(2016, 7, 8).and_hms(9, 10, 11) } - fn exampleNaiveTime() -> NaiveTime { + fn example_naive_time() -> NaiveTime { NaiveTime::from_hms(16, 7, 8) } - fn exampleDateTimeFixedOffset() -> DateTime { + fn example_date_time_fixed_offset() -> DateTime { DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00").unwrap() } - fn exampleDateTimeUtc() -> DateTime { + fn example_date_time_utc() -> DateTime { Utc.timestamp(61, 0) } } - #[crate::graphql_object] + #[graphql_object] #[cfg(not(feature = "scalar-naivetime"))] impl Root { - fn exampleNaiveDate() -> NaiveDate { + fn example_naive_date() -> NaiveDate { NaiveDate::from_ymd(2015, 3, 14) } - fn exampleNaiveDateTime() -> NaiveDateTime { + fn example_naive_date_time() -> NaiveDateTime { NaiveDate::from_ymd(2016, 7, 8).and_hms(9, 10, 11) } - fn exampleDateTimeFixedOffset() -> DateTime { + fn example_date_time_fixed_offset() -> DateTime { DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00").unwrap() } - fn exampleDateTimeUtc() -> DateTime { + fn example_date_time_utc() -> DateTime { Utc.timestamp(61, 0) } } #[cfg(feature = "scalar-naivetime")] - let doc = r#" - { + let doc = r#"{ exampleNaiveDate, exampleNaiveDateTime, exampleNaiveTime, exampleDateTimeFixedOffset, exampleDateTimeUtc, - } - "#; + }"#; #[cfg(not(feature = "scalar-naivetime"))] - let doc = r#" - { + let doc = r#"{ exampleNaiveDate, exampleNaiveDateTime, exampleDateTimeFixedOffset, exampleDateTimeUtc, - } - "#; + }"#; let schema = RootNode::new( Root, @@ -332,26 +328,26 @@ mod integration_test { assert_eq!(errs, []); + #[cfg(feature = "scalar-naivetime")] + assert_eq!( + result, + graphql_value!({ + "exampleNaiveDate": "2015-03-14", + "exampleNaiveDateTime": 1_467_969_011.0, + "exampleNaiveTime": "16:07:08", + "exampleDateTimeFixedOffset": "1996-12-19T16:39:57-08:00", + "exampleDateTimeUtc": "1970-01-01T00:01:01+00:00", + }), + ); + #[cfg(not(feature = "scalar-naivetime"))] assert_eq!( result, - Value::object( - vec![ - ("exampleNaiveDate", Value::scalar("2015-03-14")), - ("exampleNaiveDateTime", Value::scalar(1_467_969_011.0)), - #[cfg(feature = "scalar-naivetime")] - ("exampleNaiveTime", Value::scalar("16:07:08")), - ( - "exampleDateTimeFixedOffset", - Value::scalar("1996-12-19T16:39:57-08:00"), - ), - ( - "exampleDateTimeUtc", - Value::scalar("1970-01-01T00:01:01+00:00"), - ), - ] - .into_iter() - .collect() - ) + graphql_value!({ + "exampleNaiveDate": "2015-03-14", + "exampleNaiveDateTime": 1_467_969_011.0, + "exampleDateTimeFixedOffset": "1996-12-19T16:39:57-08:00", + "exampleDateTimeUtc": "1970-01-01T00:01:01+00:00", + }), ); } } diff --git a/juniper/src/integrations/mod.rs b/juniper/src/integrations/mod.rs index ad4e29e3c..76ac28ba6 100644 --- a/juniper/src/integrations/mod.rs +++ b/juniper/src/integrations/mod.rs @@ -1,24 +1,14 @@ //! Provides GraphQLType implementations for some external types -#[doc(hidden)] -pub mod serde; - +#[cfg(feature = "bson")] +pub mod bson; #[cfg(feature = "chrono")] -/// GraphQL support for [chrono](https://github.com/chronotope/chrono) types. pub mod chrono; - #[cfg(feature = "chrono-tz")] -/// GraphQL support for [chrono-tz](https://github.com/chronotope/chrono-tz) types. pub mod chrono_tz; - +#[doc(hidden)] +pub mod serde; #[cfg(feature = "url")] -/// GraphQL support for [url](https://github.com/servo/rust-url) types. pub mod url; - #[cfg(feature = "uuid")] -/// GraphQL support for [uuid](https://doc.rust-lang.org/uuid/uuid/struct.Uuid.html) types. pub mod uuid; - -#[cfg(feature = "bson")] -/// GraphQL support for [bson](https://github.com/mongodb/bson-rust) types. -pub mod bson; diff --git a/juniper/src/integrations/url.rs b/juniper/src/integrations/url.rs index 649787987..492626bd9 100644 --- a/juniper/src/integrations/url.rs +++ b/juniper/src/integrations/url.rs @@ -1,3 +1,5 @@ +//! GraphQL support for [url](https://github.com/servo/rust-url) types. + use url::Url; use crate::{ diff --git a/juniper/src/integrations/uuid.rs b/juniper/src/integrations/uuid.rs index 5a949cf7b..1f677d65d 100644 --- a/juniper/src/integrations/uuid.rs +++ b/juniper/src/integrations/uuid.rs @@ -1,3 +1,5 @@ +//! GraphQL support for [uuid](https://doc.rust-lang.org/uuid/uuid/struct.Uuid.html) types. + #![allow(clippy::needless_lifetimes)] use uuid::Uuid; diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index c4e9bd305..8c361bfef 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -172,7 +172,7 @@ pub use crate::{ types::{ async_await::{DynGraphQLValueAsync, GraphQLTypeAsync, GraphQLValueAsync}, base::{Arguments, DynGraphQLValue, GraphQLType, GraphQLValue, TypeKind}, - marker::{self, GraphQLInterface, GraphQLUnion}, + marker::{self, GraphQLInterface, GraphQLObject, GraphQLUnion}, nullable::Nullable, scalars::{EmptyMutation, EmptySubscription, ID}, subscriptions::{ diff --git a/juniper/src/macros/mod.rs b/juniper/src/macros/mod.rs index b38958dae..241878e50 100644 --- a/juniper/src/macros/mod.rs +++ b/juniper/src/macros/mod.rs @@ -1,6 +1,3 @@ //! Helper definitions for macros. pub mod helper; - -#[cfg(test)] -mod tests; diff --git a/juniper/src/macros/tests/args.rs b/juniper/src/macros/tests/args.rs deleted file mode 100644 index 9f5d038d8..000000000 --- a/juniper/src/macros/tests/args.rs +++ /dev/null @@ -1,1164 +0,0 @@ -#![allow(unused)] - -use crate::{ - executor::Variables, - schema::model::RootNode, - types::scalars::{EmptyMutation, EmptySubscription}, - value::{DefaultScalarValue, Value}, - GraphQLInputObject, -}; - -struct Root; - -/* - -Syntax to validate: - -* No args at all -* Executor arg vs. no executor arg -* Single arg vs. multi arg -* Trailing comma vs. no trailing comma -* Default value vs. no default value -* Complex default value -* Description vs. no description - -*/ - -#[derive(GraphQLInputObject, Debug)] -struct Point { - x: i32, -} - -#[crate::graphql_object] -impl Root { - fn simple() -> i32 { - 0 - } - fn exec_arg(_executor: &Executor) -> i32 { - 0 - } - fn exec_arg_and_more(_executor: &Executor, arg: i32) -> i32 { - arg - } - - fn single_arg(arg: i32) -> i32 { - arg - } - - fn multi_args(arg1: i32, arg2: i32) -> i32 { - arg1 + arg2 - } - - fn multi_args_trailing_comma(arg1: i32, arg2: i32) -> i32 { - arg1 + arg2 - } - - #[graphql(arguments(arg(description = "The arg")))] - fn single_arg_descr(arg: i32) -> i32 { - arg - } - - #[graphql(arguments(r#arg(description = "The arg")))] - fn single_arg_descr_raw_idents(arg: i32) -> i32 { - arg - } - - #[graphql(arguments( - arg1(description = "The first arg",), - arg2(description = "The second arg") - ))] - fn multi_args_descr(arg1: i32, arg2: i32) -> i32 { - arg1 + arg2 - } - - #[graphql(arguments( - r#arg1(description = "The first arg",), - r#arg2(description = "The second arg") - ))] - fn multi_args_descr_raw_idents(arg1: i32, arg2: i32) -> i32 { - arg1 + arg2 - } - - #[graphql(arguments( - arg1(description = "The first arg",), - arg2(description = "The second arg",) - ))] - fn multi_args_descr_trailing_comma(arg1: i32, arg2: i32) -> i32 { - arg1 + arg2 - } - - fn attr_arg_descr(#[graphql(description = "The arg")] arg: i32) -> i32 { - 0 - } - - fn attr_arg_descr_collapse( - #[graphql(description = "The first arg")] - #[graphql(description = "and more details")] - arg: i32, - ) -> i32 { - 0 - } - - #[graphql(arguments(arg(default = 123,),))] - fn arg_with_default(arg: i32) -> i32 { - arg - } - - #[graphql(arguments(arg1(default = 123,), arg2(default = 456,)))] - fn multi_args_with_default(arg1: i32, arg2: i32) -> i32 { - arg1 + arg2 - } - - #[graphql(arguments(arg1(default = 123,), arg2(default = 456,),))] - fn multi_args_with_default_trailing_comma(arg1: i32, arg2: i32) -> i32 { - arg1 + arg2 - } - - #[graphql(arguments(arg(default = 123, description = "The arg")))] - fn arg_with_default_descr(arg: i32) -> i32 { - arg - } - - #[graphql(arguments(r#arg(default = 123, description = "The arg")))] - fn arg_with_default_descr_raw_ident(arg: i32) -> i32 { - arg - } - - #[graphql(arguments( - arg1(default = 123, description = "The first arg"), - arg2(default = 456, description = "The second arg") - ))] - fn multi_args_with_default_descr(arg1: i32, arg2: i32) -> i32 { - arg1 + arg2 - } - - #[graphql(arguments( - r#arg1(default = 123, description = "The first arg"), - r#arg2(default = 456, description = "The second arg") - ))] - fn multi_args_with_default_descr_raw_ident(arg1: i32, arg2: i32) -> i32 { - arg1 + arg2 - } - - #[graphql(arguments( - arg1(default = 123, description = "The first arg",), - arg2(default = 456, description = "The second arg",) - ))] - fn multi_args_with_default_trailing_comma_descr(arg1: i32, arg2: i32) -> i32 { - arg1 + arg2 - } - - #[graphql(arguments( - r#arg1(default = 123, description = "The first arg",), - r#arg2(default = 456, description = "The second arg",) - ))] - fn multi_args_with_default_trailing_comma_descr_raw_ident(arg1: i32, arg2: i32) -> i32 { - arg1 + arg2 - } - - #[graphql( - arguments( - arg1( - default = "test".to_string(), - description = "A string default argument", - ), - arg2( - default = Point{ x: 1 }, - description = "An input object default argument", - ) - ), - )] - fn args_with_complex_default(arg1: String, arg2: Point) -> i32 { - let _ = arg1; - let _ = arg2; - 0 - } -} - -async fn run_args_info_query(field_name: &str, f: F) -where - F: Fn(&Vec>) -> (), -{ - let doc = r#" - { - __type(name: "Root") { - fields { - name - args { - name - description - defaultValue - type { - name - ofType { - name - } - } - } - } - } - } - "#; - let schema = RootNode::new( - Root {}, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); - - let (result, errs) = crate::execute(doc, None, &schema, &Variables::new(), &()) - .await - .expect("Execution failed"); - - assert_eq!(errs, []); - - println!("Result: {:#?}", result); - - let type_info = result - .as_object_value() - .expect("Result is not an object") - .get_field_value("__type") - .expect("__type field missing") - .as_object_value() - .expect("__type field not an object value"); - - let fields = type_info - .get_field_value("fields") - .expect("fields field missing") - .as_list_value() - .expect("fields not a list"); - - let field = fields - .into_iter() - .filter(|f| { - f.as_object_value() - .expect("Field not an object") - .get_field_value("name") - .expect("name field missing from field") - .as_scalar_value::() - .expect("name is not a string") - == field_name - }) - .next() - .expect("Field not found") - .as_object_value() - .expect("Field is not an object"); - - println!("Field: {:#?}", field); - - let args = field - .get_field_value("args") - .expect("args missing from field") - .as_list_value() - .expect("args is not a list"); - - println!("Args: {:#?}", args); - - f(args); -} - -#[tokio::test] -async fn introspect_field_simple() { - run_args_info_query("simple", |args| { - assert_eq!(args.len(), 0); - }) - .await; -} - -#[tokio::test] -async fn introspect_field_exec_arg() { - run_args_info_query("execArg", |args| { - assert_eq!(args.len(), 0); - }) - .await; -} - -#[tokio::test] -async fn introspect_field_exec_arg_and_more() { - run_args_info_query("execArgAndMore", |args| { - assert_eq!(args.len(), 1); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg")), - ("description", Value::null()), - ("defaultValue", Value::null()), - ( - "type", - Value::object( - vec![ - ("name", Value::null()), - ( - "ofType", - Value::object( - vec![("name", Value::scalar("Int"))].into_iter().collect(), - ), - ), - ] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_field_single_arg() { - run_args_info_query("singleArg", |args| { - assert_eq!(args.len(), 1); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg")), - ("description", Value::null()), - ("defaultValue", Value::null()), - ( - "type", - Value::object( - vec![ - ("name", Value::null()), - ( - "ofType", - Value::object( - vec![("name", Value::scalar("Int"))].into_iter().collect(), - ), - ), - ] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_field_multi_args() { - run_args_info_query("multiArgs", |args| { - assert_eq!(args.len(), 2); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg1")), - ("description", Value::null()), - ("defaultValue", Value::null()), - ( - "type", - Value::object( - vec![ - ("name", Value::null()), - ( - "ofType", - Value::object( - vec![("name", Value::scalar("Int"))].into_iter().collect(), - ), - ), - ] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg2")), - ("description", Value::null()), - ("defaultValue", Value::null()), - ( - "type", - Value::object( - vec![ - ("name", Value::null()), - ( - "ofType", - Value::object( - vec![("name", Value::scalar("Int"))].into_iter().collect(), - ), - ), - ] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_field_multi_args_trailing_comma() { - run_args_info_query("multiArgsTrailingComma", |args| { - assert_eq!(args.len(), 2); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg1")), - ("description", Value::null()), - ("defaultValue", Value::null()), - ( - "type", - Value::object( - vec![ - ("name", Value::null()), - ( - "ofType", - Value::object( - vec![("name", Value::scalar("Int"))].into_iter().collect(), - ), - ), - ] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg2")), - ("description", Value::null()), - ("defaultValue", Value::null()), - ( - "type", - Value::object( - vec![ - ("name", Value::null()), - ( - "ofType", - Value::object( - vec![("name", Value::scalar("Int"))].into_iter().collect(), - ), - ), - ] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_field_single_arg_descr() { - run_args_info_query("singleArgDescr", |args| { - assert_eq!(args.len(), 1); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg")), - ("description", Value::scalar("The arg")), - ("defaultValue", Value::null()), - ( - "type", - Value::object( - vec![ - ("name", Value::null()), - ( - "ofType", - Value::object( - vec![("name", Value::scalar("Int"))].into_iter().collect(), - ), - ), - ] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_field_single_arg_descr_raw_idents() { - run_args_info_query("singleArgDescrRawIdents", |args| { - assert_eq!(args.len(), 1); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg")), - ("description", Value::scalar("The arg")), - ("defaultValue", Value::null()), - ( - "type", - Value::object( - vec![ - ("name", Value::null()), - ( - "ofType", - Value::object( - vec![("name", Value::scalar("Int"))].into_iter().collect(), - ), - ), - ] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_field_multi_args_descr() { - run_args_info_query("multiArgsDescr", |args| { - assert_eq!(args.len(), 2); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg1")), - ("description", Value::scalar("The first arg")), - ("defaultValue", Value::null()), - ( - "type", - Value::object( - vec![ - ("name", Value::null()), - ( - "ofType", - Value::object( - vec![("name", Value::scalar("Int"))].into_iter().collect(), - ), - ), - ] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg2")), - ("description", Value::scalar("The second arg")), - ("defaultValue", Value::null()), - ( - "type", - Value::object( - vec![ - ("name", Value::null()), - ( - "ofType", - Value::object( - vec![("name", Value::scalar("Int"))].into_iter().collect(), - ), - ), - ] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_field_multi_args_descr_raw_idents() { - run_args_info_query("multiArgsDescrRawIdents", |args| { - assert_eq!(args.len(), 2); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg1")), - ("description", Value::scalar("The first arg")), - ("defaultValue", Value::null()), - ( - "type", - Value::object( - vec![ - ("name", Value::null()), - ( - "ofType", - Value::object( - vec![("name", Value::scalar("Int"))].into_iter().collect(), - ), - ), - ] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg2")), - ("description", Value::scalar("The second arg")), - ("defaultValue", Value::null()), - ( - "type", - Value::object( - vec![ - ("name", Value::null()), - ( - "ofType", - Value::object( - vec![("name", Value::scalar("Int"))].into_iter().collect(), - ), - ), - ] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_field_multi_args_descr_trailing_comma() { - run_args_info_query("multiArgsDescrTrailingComma", |args| { - assert_eq!(args.len(), 2); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg1")), - ("description", Value::scalar("The first arg")), - ("defaultValue", Value::null()), - ( - "type", - Value::object( - vec![ - ("name", Value::null()), - ( - "ofType", - Value::object( - vec![("name", Value::scalar("Int"))].into_iter().collect(), - ), - ), - ] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg2")), - ("description", Value::scalar("The second arg")), - ("defaultValue", Value::null()), - ( - "type", - Value::object( - vec![ - ("name", Value::null()), - ( - "ofType", - Value::object( - vec![("name", Value::scalar("Int"))].into_iter().collect(), - ), - ), - ] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_field_attr_arg_descr() { - run_args_info_query("attrArgDescr", |args| { - assert_eq!(args.len(), 1); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg")), - ("description", Value::scalar("The arg")), - ("defaultValue", Value::null()), - ( - "type", - Value::object( - vec![ - ("name", Value::null()), - ( - "ofType", - Value::object( - vec![("name", Value::scalar("Int"))].into_iter().collect(), - ), - ), - ] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }); -} - -#[tokio::test] -async fn introspect_field_attr_arg_descr_collapse() { - run_args_info_query("attrArgDescrCollapse", |args| { - assert_eq!(args.len(), 1); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg")), - ("description", Value::scalar("The arg\nand more details")), - ("defaultValue", Value::null()), - ( - "type", - Value::object( - vec![ - ("name", Value::null()), - ( - "ofType", - Value::object( - vec![("name", Value::scalar("Int"))].into_iter().collect(), - ), - ), - ] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }); -} - -#[tokio::test] -async fn introspect_field_arg_with_default() { - run_args_info_query("argWithDefault", |args| { - assert_eq!(args.len(), 1); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg")), - ("description", Value::null()), - ("defaultValue", Value::scalar("123")), - ( - "type", - Value::object( - vec![("name", Value::scalar("Int")), ("ofType", Value::null())] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_field_multi_args_with_default() { - run_args_info_query("multiArgsWithDefault", |args| { - assert_eq!(args.len(), 2); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg1")), - ("description", Value::null()), - ("defaultValue", Value::scalar("123")), - ( - "type", - Value::object( - vec![("name", Value::scalar("Int")), ("ofType", Value::null())] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg2")), - ("description", Value::null()), - ("defaultValue", Value::scalar("456")), - ( - "type", - Value::object( - vec![("name", Value::scalar("Int")), ("ofType", Value::null())] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_field_multi_args_with_default_trailing_comma() { - run_args_info_query("multiArgsWithDefaultTrailingComma", |args| { - assert_eq!(args.len(), 2); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg1")), - ("description", Value::null()), - ("defaultValue", Value::scalar("123")), - ( - "type", - Value::object( - vec![("name", Value::scalar("Int")), ("ofType", Value::null())] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg2")), - ("description", Value::null()), - ("defaultValue", Value::scalar("456")), - ( - "type", - Value::object( - vec![("name", Value::scalar("Int")), ("ofType", Value::null())] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_field_arg_with_default_descr() { - run_args_info_query("argWithDefaultDescr", |args| { - assert_eq!(args.len(), 1); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg")), - ("description", Value::scalar("The arg")), - ("defaultValue", Value::scalar("123")), - ( - "type", - Value::object( - vec![("name", Value::scalar("Int")), ("ofType", Value::null())] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_field_arg_with_default_descr_raw_ident() { - run_args_info_query("argWithDefaultDescrRawIdent", |args| { - assert_eq!(args.len(), 1); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg")), - ("description", Value::scalar("The arg")), - ("defaultValue", Value::scalar("123")), - ( - "type", - Value::object( - vec![("name", Value::scalar("Int")), ("ofType", Value::null())] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_field_multi_args_with_default_descr() { - run_args_info_query("multiArgsWithDefaultDescr", |args| { - assert_eq!(args.len(), 2); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg1")), - ("description", Value::scalar("The first arg")), - ("defaultValue", Value::scalar("123")), - ( - "type", - Value::object( - vec![("name", Value::scalar("Int")), ("ofType", Value::null())] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg2")), - ("description", Value::scalar("The second arg")), - ("defaultValue", Value::scalar("456")), - ( - "type", - Value::object( - vec![("name", Value::scalar("Int")), ("ofType", Value::null())] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_field_multi_args_with_default_descr_raw_ident() { - run_args_info_query("multiArgsWithDefaultDescrRawIdent", |args| { - assert_eq!(args.len(), 2); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg1")), - ("description", Value::scalar("The first arg")), - ("defaultValue", Value::scalar("123")), - ( - "type", - Value::object( - vec![("name", Value::scalar("Int")), ("ofType", Value::null())] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg2")), - ("description", Value::scalar("The second arg")), - ("defaultValue", Value::scalar("456")), - ( - "type", - Value::object( - vec![("name", Value::scalar("Int")), ("ofType", Value::null())] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_field_multi_args_with_default_trailing_comma_descr() { - run_args_info_query("multiArgsWithDefaultTrailingCommaDescr", |args| { - assert_eq!(args.len(), 2); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg1")), - ("description", Value::scalar("The first arg")), - ("defaultValue", Value::scalar("123")), - ( - "type", - Value::object( - vec![("name", Value::scalar("Int")), ("ofType", Value::null())] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg2")), - ("description", Value::scalar("The second arg")), - ("defaultValue", Value::scalar("456")), - ( - "type", - Value::object( - vec![("name", Value::scalar("Int")), ("ofType", Value::null())] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_field_multi_args_with_default_trailing_comma_descr_raw_ident() { - run_args_info_query("multiArgsWithDefaultTrailingCommaDescrRawIdent", |args| { - assert_eq!(args.len(), 2); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg1")), - ("description", Value::scalar("The first arg")), - ("defaultValue", Value::scalar("123")), - ( - "type", - Value::object( - vec![("name", Value::scalar("Int")), ("ofType", Value::null())] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg2")), - ("description", Value::scalar("The second arg")), - ("defaultValue", Value::scalar("456")), - ( - "type", - Value::object( - vec![("name", Value::scalar("Int")), ("ofType", Value::null())] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_field_args_with_complex_default() { - run_args_info_query("argsWithComplexDefault", |args| { - assert_eq!(args.len(), 2); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg1")), - ("description", Value::scalar("A string default argument")), - ("defaultValue", Value::scalar(r#""test""#)), - ( - "type", - Value::object( - vec![("name", Value::scalar("String")), ("ofType", Value::null())] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - - assert!(args.contains(&Value::object( - vec![ - ("name", Value::scalar("arg2")), - ( - "description", - Value::scalar("An input object default argument"), - ), - ("defaultValue", Value::scalar(r#"{x: 1}"#)), - ( - "type", - Value::object( - vec![("name", Value::scalar("Point")), ("ofType", Value::null())] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ))); - }) - .await; -} diff --git a/juniper/src/macros/tests/field.rs b/juniper/src/macros/tests/field.rs deleted file mode 100644 index e1a73039f..000000000 --- a/juniper/src/macros/tests/field.rs +++ /dev/null @@ -1,731 +0,0 @@ -/* - -Syntax to validate: - -* Object vs. interface -* Description vs. no description -* Deprecated vs. not deprecated -* FieldResult vs. object directly -* Return vs. implicit return - -*/ - -#![allow(deprecated)] - -use crate::{ - ast::InputValue, - executor::FieldResult, - graphql_interface, graphql_object, - schema::model::RootNode, - types::scalars::{EmptyMutation, EmptySubscription}, - value::{DefaultScalarValue, Object, Value}, -}; - -#[derive(Debug)] -struct Root; - -#[graphql_object(impl = InterfaceValue)] -impl Root { - fn simple() -> i32 { - 0 - } - - /// Field description - fn description() -> i32 { - 0 - } - - #[deprecated] - fn deprecated_outer() -> bool { - true - } - - #[deprecated(note = "Deprecation Reason")] - fn deprecated_outer_with_reason() -> bool { - true - } - - #[graphql(deprecated = "Deprecation reason")] - fn deprecated() -> i32 { - 0 - } - - #[graphql(deprecated = "Deprecation reason", description = "Field description")] - fn deprecated_descr() -> i32 { - 0 - } - - /// Field description - fn attr_description() -> i32 { - 0 - } - - /// Field description - /// with `collapse_docs` behavior - fn attr_description_collapse() -> i32 { - 0 - } - - /// Get the i32 representation of 0. - /// - /// - This comment is longer. - /// - These two lines are rendered as bullets by GraphiQL. - /// - subsection - fn attr_description_long() -> i32 { - 0 - } - - #[graphql(deprecated)] - fn attr_deprecated() -> i32 { - 0 - } - - #[graphql(deprecated = "Deprecation reason")] - fn attr_deprecated_reason() -> i32 { - 0 - } - - /// Field description - #[graphql(deprecated = "Deprecation reason")] - fn attr_deprecated_descr() -> i32 { - 0 - } - - fn with_field_result() -> FieldResult { - Ok(0) - } - - fn with_return() -> i32 { - 0 - } - - fn with_return_field_result() -> FieldResult { - Ok(0) - } -} - -#[graphql_interface] -impl Interface for Root { - fn simple(&self) -> i32 { - 0 - } - - fn description(&self) -> i32 { - 0 - } - - fn deprecated(&self) -> i32 { - 0 - } - - fn deprecated_descr(&self) -> i32 { - 0 - } - - fn attr_description(&self) -> i32 { - 0 - } - - fn attr_description_collapse(&self) -> i32 { - 0 - } - - fn attr_description_long(&self) -> i32 { - 0 - } - - fn attr_deprecated(&self) -> i32 { - 0 - } - - fn attr_deprecated_reason(&self) -> i32 { - 0 - } - - fn attr_deprecated_descr(&self) -> i32 { - 0 - } -} - -#[graphql_interface(for = Root)] -trait Interface { - fn simple(&self) -> i32; - - #[graphql(desc = "Field description")] - fn description(&self) -> i32; - - #[graphql(deprecated = "Deprecation reason")] - fn deprecated(&self) -> i32; - - #[graphql(desc = "Field description", deprecated = "Deprecation reason")] - fn deprecated_descr(&self) -> i32; - - /// Field description - fn attr_description(&self) -> i32; - - /// Field description - /// with `collapse_docs` behavior - fn attr_description_collapse(&self) -> i32; - - /// Get the i32 representation of 0. - /// - /// - This comment is longer. - /// - These two lines are rendered as bullets by GraphiQL. - fn attr_description_long(&self) -> i32; - - #[deprecated] - fn attr_deprecated(&self) -> i32; - - #[deprecated(note = "Deprecation reason")] - fn attr_deprecated_reason(&self) -> i32; - - /// Field description - #[deprecated(note = "Deprecation reason")] - fn attr_deprecated_descr(&self) -> i32; -} - -async fn run_field_info_query(type_name: &str, field_name: &str, f: F) -where - F: Fn(&Object) -> (), -{ - let doc = r#" - query ($typeName: String!) { - __type(name: $typeName) { - fields(includeDeprecated: true) { - name - description - isDeprecated - deprecationReason - } - } - } - "#; - let schema = RootNode::new( - Root {}, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); - let vars = vec![("typeName".to_owned(), InputValue::scalar(type_name))] - .into_iter() - .collect(); - - let (result, errs) = crate::execute(doc, None, &schema, &vars, &()) - .await - .expect("Execution failed"); - - assert_eq!(errs, []); - - println!("Result: {:#?}", result); - - let type_info = result - .as_object_value() - .expect("Result is not an object") - .get_field_value("__type") - .expect("__type field missing") - .as_object_value() - .expect("__type field not an object value"); - - let fields = type_info - .get_field_value("fields") - .expect("fields field missing") - .as_list_value() - .expect("fields not a list"); - - let field = fields - .into_iter() - .filter(|f| { - f.as_object_value() - .expect("Field not an object") - .get_field_value("name") - .expect("name field missing from field") - .as_scalar_value::() - .expect("name is not a string") - == field_name - }) - .next() - .expect("Field not found") - .as_object_value() - .expect("Field is not an object"); - - println!("Field: {:#?}", field); - - f(field); -} - -#[tokio::test] -async fn introspect_object_field_simple() { - run_field_info_query("Root", "simple", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("simple")) - ); - assert_eq!(field.get_field_value("description"), Some(&Value::null())); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(false)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::null()) - ); - }) - .await; -} - -#[tokio::test] -async fn introspect_interface_field_simple() { - run_field_info_query("Interface", "simple", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("simple")) - ); - assert_eq!(field.get_field_value("description"), Some(&Value::null())); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(false)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::null()) - ); - }) - .await; -} - -#[tokio::test] -async fn introspect_object_field_description() { - run_field_info_query("Root", "description", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("description")) - ); - assert_eq!( - field.get_field_value("description"), - Some(&Value::scalar("Field description")) - ); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(false)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::null()) - ); - }) - .await; -} - -#[tokio::test] -async fn introspect_interface_field_description() { - run_field_info_query("Interface", "description", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("description")) - ); - assert_eq!( - field.get_field_value("description"), - Some(&Value::scalar("Field description")) - ); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(false)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::null()) - ); - }) - .await; -} - -#[tokio::test] -async fn introspect_object_field_deprecated_outer() { - run_field_info_query("Root", "deprecatedOuter", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("deprecatedOuter")) - ); - assert_eq!(field.get_field_value("description"), Some(&Value::null())); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(true)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::null()), - ); - }) - .await; -} - -#[tokio::test] -async fn introspect_object_field_deprecated_outer_with_reason() { - run_field_info_query("Root", "deprecatedOuterWithReason", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("deprecatedOuterWithReason")) - ); - assert_eq!(field.get_field_value("description"), Some(&Value::null())); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(true)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::scalar("Deprecation Reason")), - ); - }) - .await; -} - -#[tokio::test] -async fn introspect_object_field_deprecated() { - run_field_info_query("Root", "deprecated", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("deprecated")) - ); - assert_eq!(field.get_field_value("description"), Some(&Value::null())); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(true)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::scalar("Deprecation reason")) - ); - }) - .await; -} - -#[tokio::test] -async fn introspect_interface_field_deprecated() { - run_field_info_query("Interface", "deprecated", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("deprecated")) - ); - assert_eq!(field.get_field_value("description"), Some(&Value::null())); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(true)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::scalar("Deprecation reason")) - ); - }) - .await; -} - -#[tokio::test] -async fn introspect_object_field_deprecated_descr() { - run_field_info_query("Root", "deprecatedDescr", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("deprecatedDescr")) - ); - assert_eq!( - field.get_field_value("description"), - Some(&Value::scalar("Field description")) - ); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(true)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::scalar("Deprecation reason")) - ); - }) - .await; -} - -#[tokio::test] -async fn introspect_interface_field_deprecated_descr() { - run_field_info_query("Interface", "deprecatedDescr", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("deprecatedDescr")) - ); - assert_eq!( - field.get_field_value("description"), - Some(&Value::scalar("Field description")) - ); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(true)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::scalar("Deprecation reason")) - ); - }) - .await; -} - -#[tokio::test] -async fn introspect_object_field_attr_description() { - run_field_info_query("Root", "attrDescription", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("attrDescription")) - ); - assert_eq!( - field.get_field_value("description"), - Some(&Value::scalar("Field description")) - ); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(false)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::null()) - ); - }) - .await; -} - -#[tokio::test] -async fn introspect_interface_field_attr_description() { - run_field_info_query("Interface", "attrDescription", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("attrDescription")) - ); - assert_eq!( - field.get_field_value("description"), - Some(&Value::scalar("Field description")) - ); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(false)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::null()) - ); - }) - .await; -} - -#[tokio::test] -async fn introspect_object_field_attr_description_long() { - run_field_info_query("Root", "attrDescriptionLong", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("attrDescriptionLong")) - ); - assert_eq!( - field.get_field_value("description"), - Some(&Value::scalar("Get the i32 representation of 0.\n\n- This comment is longer.\n- These two lines are rendered as bullets by GraphiQL.\n - subsection")) - ); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(false)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::null()) - ); - }).await; -} - -#[tokio::test] -async fn introspect_interface_field_attr_description_long() { - run_field_info_query("Interface", "attrDescriptionLong", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("attrDescriptionLong")) - ); - assert_eq!( - field.get_field_value("description"), - Some(&Value::scalar("Get the i32 representation of 0.\n\n- This comment is longer.\n- These two lines are rendered as bullets by GraphiQL.")) - ); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(false)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::null()) - ); - }).await; -} - -#[tokio::test] -async fn introspect_object_field_attr_description_collapse() { - run_field_info_query("Root", "attrDescriptionCollapse", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("attrDescriptionCollapse")) - ); - assert_eq!( - field.get_field_value("description"), - Some(&Value::scalar( - "Field description\nwith `collapse_docs` behavior" - )) - ); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(false)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::null()) - ); - }) - .await; -} - -#[tokio::test] -async fn introspect_interface_field_attr_description_collapse() { - run_field_info_query("Interface", "attrDescriptionCollapse", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("attrDescriptionCollapse")) - ); - assert_eq!( - field.get_field_value("description"), - Some(&Value::scalar( - "Field description\nwith `collapse_docs` behavior" - )) - ); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(false)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::null()) - ); - }) - .await; -} - -#[tokio::test] -async fn introspect_object_field_attr_deprecated() { - run_field_info_query("Root", "attrDeprecated", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("attrDeprecated")) - ); - assert_eq!(field.get_field_value("description"), Some(&Value::null())); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(true)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::null()) - ); - }) - .await; -} - -#[tokio::test] -async fn introspect_interface_field_attr_deprecated() { - run_field_info_query("Interface", "attrDeprecated", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("attrDeprecated")) - ); - assert_eq!(field.get_field_value("description"), Some(&Value::null())); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(true)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::null()) - ); - }) - .await; -} - -#[tokio::test] -async fn introspect_object_field_attr_deprecated_reason() { - run_field_info_query("Root", "attrDeprecatedReason", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("attrDeprecatedReason")) - ); - assert_eq!(field.get_field_value("description"), Some(&Value::null())); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(true)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::scalar("Deprecation reason")) - ); - }) - .await; -} - -#[tokio::test] -async fn introspect_interface_field_attr_deprecated_reason() { - run_field_info_query("Interface", "attrDeprecatedReason", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("attrDeprecatedReason")) - ); - assert_eq!(field.get_field_value("description"), Some(&Value::null())); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(true)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::scalar("Deprecation reason")) - ); - }) - .await; -} - -#[tokio::test] -async fn introspect_object_field_attr_deprecated_descr() { - run_field_info_query("Root", "attrDeprecatedDescr", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("attrDeprecatedDescr")) - ); - assert_eq!( - field.get_field_value("description"), - Some(&Value::scalar("Field description")) - ); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(true)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::scalar("Deprecation reason")) - ); - }) - .await; -} - -#[tokio::test] -async fn introspect_interface_field_attr_deprecated_descr() { - run_field_info_query("Interface", "attrDeprecatedDescr", |field| { - assert_eq!( - field.get_field_value("name"), - Some(&Value::scalar("attrDeprecatedDescr")) - ); - assert_eq!( - field.get_field_value("description"), - Some(&Value::scalar("Field description")) - ); - assert_eq!( - field.get_field_value("isDeprecated"), - Some(&Value::scalar(true)) - ); - assert_eq!( - field.get_field_value("deprecationReason"), - Some(&Value::scalar("Deprecation reason")) - ); - }) - .await; -} diff --git a/juniper/src/macros/tests/impl_object.rs b/juniper/src/macros/tests/impl_object.rs deleted file mode 100644 index d82607da7..000000000 --- a/juniper/src/macros/tests/impl_object.rs +++ /dev/null @@ -1,308 +0,0 @@ -use super::util; -use crate::{graphql_value, EmptyMutation, EmptySubscription, RootNode}; - -#[derive(Default)] -struct Context { - flag1: bool, -} - -impl crate::Context for Context {} - -struct WithLifetime<'a> { - value: &'a str, -} - -#[crate::graphql_object(Context=Context)] -impl<'a> WithLifetime<'a> { - fn value(&'a self) -> &'a str { - self.value - } -} - -struct WithContext; - -#[crate::graphql_object(Context=Context)] -impl WithContext { - fn ctx(ctx: &Context) -> bool { - ctx.flag1 - } -} - -#[derive(Default)] -struct Query { - b: bool, -} - -#[crate::graphql_object( - scalar = crate::DefaultScalarValue, - name = "Query", - context = Context, - // FIXME: make async work - noasync -)] -/// Query Description. -impl<'a> Query { - #[graphql(description = "With Self Description")] - fn with_self(&self) -> bool { - self.b - } - - fn independent() -> i32 { - 100 - } - - fn with_executor(_exec: &Executor) -> bool { - true - } - - fn with_executor_and_self(&self, _exec: &Executor) -> bool { - true - } - - fn with_context(_context: &Context) -> bool { - true - } - - fn with_context_and_self(&self, _context: &Context) -> bool { - true - } - - #[graphql(name = "renamed")] - fn has_custom_name() -> bool { - true - } - - #[graphql(description = "attr")] - fn has_description_attr() -> bool { - true - } - - /// Doc description - fn has_description_doc_comment() -> bool { - true - } - - fn has_argument(arg1: bool) -> bool { - arg1 - } - - #[graphql(arguments(default_arg(default = true)))] - fn default_argument(default_arg: bool) -> bool { - default_arg - } - - #[graphql(arguments(arg(description = "my argument description")))] - fn arg_with_description(arg: bool) -> bool { - arg - } - - fn with_context_child(&self) -> WithContext { - WithContext - } - - fn with_lifetime_child(&self) -> WithLifetime<'a> { - WithLifetime { value: "blub" } - } - - fn with_mut_arg(mut arg: bool) -> bool { - if arg { - arg = !arg; - } - arg - } -} - -#[derive(Default)] -struct Mutation; - -#[crate::graphql_object(context = Context)] -impl Mutation { - fn empty() -> bool { - true - } -} - -#[derive(Default)] -struct Subscription; - -#[crate::graphql_object(context = Context)] -impl Subscription { - fn empty() -> bool { - true - } -} - -#[tokio::test] -async fn object_introspect() { - let res = util::run_info_query::("Query").await; - assert_eq!( - res, - crate::graphql_value!({ - "name": "Query", - "description": "Query Description.", - "fields": [ - { - "name": "withSelf", - "description": "With Self Description", - "args": [], - }, - { - "name": "independent", - "description": None, - "args": [], - }, - { - "name": "withExecutor", - "description": None, - "args": [], - }, - { - "name": "withExecutorAndSelf", - "description": None, - "args": [], - }, - { - "name": "withContext", - "description": None, - "args": [], - }, - { - "name": "withContextAndSelf", - "description": None, - "args": [], - }, - { - "name": "renamed", - "description": None, - "args": [], - }, - { - "name": "hasDescriptionAttr", - "description": "attr", - "args": [], - }, - { - "name": "hasDescriptionDocComment", - "description": "Doc description", - "args": [], - }, - { - "name": "hasArgument", - "description": None, - "args": [ - { - "name": "arg1", - "description": None, - "type": { - "name": None, - }, - } - ], - }, - { - "name": "defaultArgument", - "description": None, - "args": [ - { - "name": "defaultArg", - "description": None, - "type": { - "name": "Boolean", - }, - } - ], - }, - { - "name": "argWithDescription", - "description": None, - "args": [ - { - "name": "arg", - "description": "my argument description", - "type": { - "name": None - }, - } - ], - }, - { - "name": "withContextChild", - "description": None, - "args": [], - }, - { - "name": "withLifetimeChild", - "description": None, - "args": [], - }, - { - "name": "withMutArg", - "description": None, - "args": [ - { - "name": "arg", - "description": None, - "type": { - "name": None, - }, - } - ], - }, - ] - }) - ); -} - -#[tokio::test] -async fn object_query() { - let doc = r#" - query { - withSelf - independent - withExecutor - withExecutorAndSelf - withContext - withContextAndSelf - renamed - hasArgument(arg1: true) - defaultArgument - argWithDescription(arg: true) - withContextChild { - ctx - } - withLifetimeChild { - value - } - withMutArg(arg: true) - } - "#; - let schema = RootNode::new( - Query { b: true }, - EmptyMutation::::new(), - EmptySubscription::::new(), - ); - let vars = std::collections::HashMap::new(); - - let (result, errs) = crate::execute(doc, None, &schema, &vars, &Context { flag1: true }) - .await - .expect("Execution failed"); - assert_eq!(errs, []); - assert_eq!( - result, - graphql_value!({ - "withSelf": true, - "independent": 100, - "withExecutor": true, - "withExecutorAndSelf": true, - "withContext": true, - "withContextAndSelf": true, - "renamed": true, - "hasArgument": true, - "defaultArgument": true, - "argWithDescription": true, - "withContextChild": { "ctx": true }, - "withLifetimeChild": { "value": "blub" }, - "withMutArg": false, - }) - ); -} diff --git a/juniper/src/macros/tests/impl_subscription.rs b/juniper/src/macros/tests/impl_subscription.rs deleted file mode 100644 index de56473c2..000000000 --- a/juniper/src/macros/tests/impl_subscription.rs +++ /dev/null @@ -1,355 +0,0 @@ -use std::pin::Pin; - -use futures::StreamExt as _; - -use crate::{graphql_value, EmptyMutation, RootNode, Value}; - -use super::util; - -#[derive(Default)] -struct Context { - flag1: bool, -} - -impl crate::Context for Context {} - -struct WithLifetime<'a> { - value: &'a str, -} - -#[crate::graphql_object(Context = Context)] -impl<'a> WithLifetime<'a> { - fn value(&'a self) -> &'a str { - self.value - } -} - -struct WithContext; - -#[crate::graphql_object(Context = Context)] -impl WithContext { - fn ctx(ctx: &Context) -> bool { - ctx.flag1 - } -} - -#[derive(Default)] -struct Query; - -#[crate::graphql_object( - Context = Context, -)] -impl Query { - fn empty() -> bool { - true - } -} - -#[derive(Default)] -struct Mutation; - -#[crate::graphql_object(context = Context)] -impl Mutation { - fn empty() -> bool { - true - } -} - -type Stream = Pin + Send>>; - -#[derive(Default)] -struct Subscription { - b: bool, -} - -#[crate::graphql_subscription( - scalar = crate::DefaultScalarValue, - name = "Subscription", - context = Context, -)] -/// Subscription Description. -impl Subscription { - #[graphql(description = "With Self Description")] - async fn with_self(&self) -> Stream { - let b = self.b; - Box::pin(futures::stream::once(async move { b })) - } - - async fn independent() -> Stream { - Box::pin(futures::stream::once(async { 100 })) - } - - async fn with_executor(_exec: &Executor) -> Stream { - Box::pin(futures::stream::once(async { true })) - } - - async fn with_executor_and_self(&self, _exec: &Executor) -> Stream { - Box::pin(futures::stream::once(async { true })) - } - - async fn with_context(_context: &Context) -> Stream { - Box::pin(futures::stream::once(async { true })) - } - - async fn with_context_and_self(&self, _context: &Context) -> Stream { - Box::pin(futures::stream::once(async { true })) - } - - #[graphql(name = "renamed")] - async fn has_custom_name() -> Stream { - Box::pin(futures::stream::once(async { true })) - } - - #[graphql(description = "attr")] - async fn has_description_attr() -> Stream { - Box::pin(futures::stream::once(async { true })) - } - - /// Doc description - async fn has_description_doc_comment() -> Stream { - Box::pin(futures::stream::once(async { true })) - } - - async fn has_argument(arg1: bool) -> Stream { - Box::pin(futures::stream::once(async move { arg1 })) - } - - #[graphql(arguments(default_arg(default = true)))] - async fn default_argument(default_arg: bool) -> Stream { - Box::pin(futures::stream::once(async move { default_arg })) - } - - #[graphql(arguments(arg(description = "my argument description")))] - async fn arg_with_description(arg: bool) -> Stream { - Box::pin(futures::stream::once(async move { arg })) - } - - async fn with_context_child(&self) -> Stream { - Box::pin(futures::stream::once(async { WithContext })) - } - - async fn with_implicit_lifetime_child(&self) -> Stream> { - Box::pin(futures::stream::once(async { - WithLifetime { value: "blub" } - })) - } - - async fn with_mut_arg(mut arg: bool) -> Stream { - if arg { - arg = !arg; - } - - Box::pin(futures::stream::once(async move { arg })) - } - - async fn without_type_alias() -> Pin + Send>> { - Box::pin(futures::stream::once(async { "abc" })) - } -} - -#[tokio::test] -async fn object_introspect() { - let res = util::run_info_query::("Subscription").await; - assert_eq!( - res, - crate::graphql_value!({ - "name": "Subscription", - "description": "Subscription Description.", - "fields": [ - { - "name": "withSelf", - "description": "With Self Description", - "args": [], - }, - { - "name": "independent", - "description": None, - "args": [], - }, - { - "name": "withExecutor", - "description": None, - "args": [], - }, - { - "name": "withExecutorAndSelf", - "description": None, - "args": [], - }, - { - "name": "withContext", - "description": None, - "args": [], - }, - { - "name": "withContextAndSelf", - "description": None, - "args": [], - }, - { - "name": "renamed", - "description": None, - "args": [], - }, - { - "name": "hasDescriptionAttr", - "description": "attr", - "args": [], - }, - { - "name": "hasDescriptionDocComment", - "description": "Doc description", - "args": [], - }, - { - "name": "hasArgument", - "description": None, - "args": [ - { - "name": "arg1", - "description": None, - "type": { - "name": None, - }, - } - ], - }, - { - "name": "defaultArgument", - "description": None, - "args": [ - { - "name": "defaultArg", - "description": None, - "type": { - "name": "Boolean", - }, - } - ], - }, - { - "name": "argWithDescription", - "description": None, - "args": [ - { - "name": "arg", - "description": "my argument description", - "type": { - "name": None - }, - } - ], - }, - { - "name": "withContextChild", - "description": None, - "args": [], - }, - { - "name": "withImplicitLifetimeChild", - "description": None, - "args": [], - }, - { - "name": "withMutArg", - "description": None, - "args": [ - { - "name": "arg", - "description": None, - "type": { - "name": None, - }, - } - ], - }, - { - "name": "withoutTypeAlias", - "description": None, - "args": [], - } - ] - }) - ); -} - -#[tokio::test] -async fn object_query() { - let doc = r#" - subscription { - withSelf - independent - withExecutor - withExecutorAndSelf - withContext - withContextAndSelf - renamed - hasArgument(arg1: true) - defaultArgument - argWithDescription(arg: true) - withContextChild { - ctx - } - withImplicitLifetimeChild { - value - } - withMutArg(arg: true) - withoutTypeAlias - } - "#; - let schema = RootNode::new( - Query, - EmptyMutation::::new(), - Subscription { b: true }, - ); - let vars = std::collections::HashMap::new(); - - let (stream_val, errs) = - crate::resolve_into_stream(doc, None, &schema, &vars, &Context { flag1: true }) - .await - .expect("Execution failed"); - - let result = if let Value::Object(obj) = stream_val { - let mut result = Vec::new(); - for (name, mut val) in obj { - if let Value::Scalar(ref mut stream) = val { - let first = stream - .next() - .await - .expect("Stream does not have the first element") - .expect(&format!("Error resolving {} field", name)); - result.push((name, first)) - } - } - result - } else { - panic!("Expected to get Value::Object ") - }; - - assert_eq!(errs, []); - assert_eq!( - result, - vec![ - ("withSelf".to_string(), graphql_value!(true)), - ("independent".to_string(), graphql_value!(100)), - ("withExecutor".to_string(), graphql_value!(true)), - ("withExecutorAndSelf".to_string(), graphql_value!(true)), - ("withContext".to_string(), graphql_value!(true)), - ("withContextAndSelf".to_string(), graphql_value!(true)), - ("renamed".to_string(), graphql_value!(true)), - ("hasArgument".to_string(), graphql_value!(true)), - ("defaultArgument".to_string(), graphql_value!(true)), - ("argWithDescription".to_string(), graphql_value!(true)), - ( - "withContextChild".to_string(), - graphql_value!({"ctx": true}) - ), - ( - "withImplicitLifetimeChild".to_string(), - graphql_value!({ "value": "blub" }) - ), - ("withMutArg".to_string(), graphql_value!(false)), - ("withoutTypeAlias".to_string(), graphql_value!("abc")), - ] - ); -} diff --git a/juniper/src/macros/tests/interface.rs b/juniper/src/macros/tests/interface.rs deleted file mode 100644 index 0f2eb3a57..000000000 --- a/juniper/src/macros/tests/interface.rs +++ /dev/null @@ -1,218 +0,0 @@ -/* - -Syntax to validate: - -* Order of items: fields, description, instance resolvers -* Optional Generics/lifetimes -* Custom name vs. default name -* Optional commas between items -* Optional trailing commas on instance resolvers - -*/ - -use crate::{ - ast::InputValue, - graphql_interface, graphql_object, - schema::model::RootNode, - types::scalars::{EmptyMutation, EmptySubscription}, - value::{DefaultScalarValue, Object, Value}, -}; - -struct Concrete; - -#[graphql_object(impl = [ - CustomNameValue, DescriptionValue, WithLifetimeValue<'_>, WithGenericsValue<()>, -])] -impl Concrete { - fn simple() -> i32 { - 0 - } -} - -#[graphql_interface(for = Concrete, name = "ACustomNamedInterface")] -trait CustomName { - fn simple(&self) -> i32; -} -#[graphql_interface] -impl CustomName for Concrete { - fn simple(&self) -> i32 { - 0 - } -} - -#[graphql_interface(for = Concrete)] -trait WithLifetime<'a> { - fn simple(&self) -> i32; -} -#[graphql_interface] -impl<'a> WithLifetime<'a> for Concrete { - fn simple(&self) -> i32 { - 0 - } -} - -#[graphql_interface(for = Concrete)] -trait WithGenerics { - fn simple(&self) -> i32; -} -#[graphql_interface] -impl WithGenerics for Concrete { - fn simple(&self) -> i32 { - 0 - } -} - -#[graphql_interface(for = Concrete, desc = "A description")] -trait Description { - fn simple(&self) -> i32; -} -#[graphql_interface] -impl Description for Concrete { - fn simple(&self) -> i32 { - 0 - } -} - -struct Root; - -#[graphql_object] -impl Root { - fn custom_name() -> CustomNameValue { - Concrete.into() - } - - fn with_lifetime() -> WithLifetimeValue<'static> { - Concrete.into() - } - fn with_generics() -> WithGenericsValue { - Concrete.into() - } - - fn description() -> DescriptionValue { - Concrete.into() - } -} - -async fn run_type_info_query(type_name: &str, f: F) -where - F: Fn(&Object, &Vec>) -> (), -{ - let doc = r#" - query ($typeName: String!) { - __type(name: $typeName) { - name - description - fields(includeDeprecated: true) { - name - } - } - } - "#; - let schema = RootNode::new( - Root {}, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); - let vars = vec![("typeName".to_owned(), InputValue::scalar(type_name))] - .into_iter() - .collect(); - - let (result, errs) = crate::execute(doc, None, &schema, &vars, &()) - .await - .expect("Execution failed"); - - assert_eq!(errs, []); - - println!("Result: {:#?}", result); - - let type_info = result - .as_object_value() - .expect("Result is not an object") - .get_field_value("__type") - .expect("__type field missing") - .as_object_value() - .expect("__type field not an object value"); - - let fields = type_info - .get_field_value("fields") - .expect("fields field missing") - .as_list_value() - .expect("fields field not a list value"); - - f(type_info, fields); -} - -#[tokio::test] -async fn introspect_custom_name() { - run_type_info_query("ACustomNamedInterface", |object, fields| { - assert_eq!( - object.get_field_value("name"), - Some(&Value::scalar("ACustomNamedInterface")) - ); - assert_eq!(object.get_field_value("description"), Some(&Value::null())); - - assert!(fields.contains(&Value::object( - vec![("name", Value::scalar("simple"))] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_with_lifetime() { - run_type_info_query("WithLifetime", |object, fields| { - assert_eq!( - object.get_field_value("name"), - Some(&Value::scalar("WithLifetime")) - ); - assert_eq!(object.get_field_value("description"), Some(&Value::null())); - - assert!(fields.contains(&Value::object( - vec![("name", Value::scalar("simple"))] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_with_generics() { - run_type_info_query("WithGenerics", |object, fields| { - assert_eq!( - object.get_field_value("name"), - Some(&Value::scalar("WithGenerics")) - ); - assert_eq!(object.get_field_value("description"), Some(&Value::null())); - - assert!(fields.contains(&Value::object( - vec![("name", Value::scalar("simple"))] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_description_first() { - run_type_info_query("Description", |object, fields| { - assert_eq!( - object.get_field_value("name"), - Some(&Value::scalar("Description")) - ); - assert_eq!( - object.get_field_value("description"), - Some(&Value::scalar("A description")) - ); - - assert!(fields.contains(&Value::object( - vec![("name", Value::scalar("simple"))] - .into_iter() - .collect(), - ))); - }) - .await; -} diff --git a/juniper/src/macros/tests/mod.rs b/juniper/src/macros/tests/mod.rs deleted file mode 100644 index 795133254..000000000 --- a/juniper/src/macros/tests/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod args; -mod field; -mod impl_object; -mod impl_subscription; -mod interface; -mod object; -mod union; -mod util; diff --git a/juniper/src/macros/tests/object.rs b/juniper/src/macros/tests/object.rs deleted file mode 100644 index 8a10ec644..000000000 --- a/juniper/src/macros/tests/object.rs +++ /dev/null @@ -1,334 +0,0 @@ -/* -Syntax to validate: - -* Order of items: fields, description, interfaces -* Optional Generics/lifetimes -* Custom name vs. default name -* Optional commas between items -* Nullable/fallible context switching -*/ - -#![allow(dead_code)] - -use std::marker::PhantomData; - -use crate::{ - ast::InputValue, - executor::{Context, FieldResult}, - graphql_interface, graphql_object, - schema::model::RootNode, - types::scalars::{EmptyMutation, EmptySubscription}, - value::{DefaultScalarValue, Object, Value}, - GraphQLObject, -}; - -struct CustomName; - -#[graphql_object(name = "ACustomNamedType")] -impl CustomName { - fn simple() -> i32 { - 0 - } -} - -struct WithLifetime<'a> { - data: PhantomData<&'a i32>, -} - -#[graphql_object] -impl<'a> WithLifetime<'a> { - fn simple() -> i32 { - 0 - } -} - -struct WithGenerics { - data: T, -} - -#[graphql_object] -impl WithGenerics { - fn simple() -> i32 { - 0 - } -} - -#[graphql_interface(for = SimpleObject)] -trait Interface { - fn simple(&self) -> i32 { - 0 - } -} - -#[derive(GraphQLObject)] -#[graphql(impl = InterfaceValue, description = "A description")] -struct SimpleObject { - simple: i32, -} - -#[graphql_interface] -impl Interface for SimpleObject {} - -struct InnerContext; -impl Context for InnerContext {} - -#[derive(GraphQLObject)] -#[graphql(context = InnerContext)] -struct InnerType { - a: i32, -} - -struct CtxSwitcher; - -#[graphql_object(context = InnerContext)] -impl CtxSwitcher { - fn ctx_switch_always() -> (&InnerContext, InnerType) { - (executor.context(), InnerType { a: 0 }) - } - - fn ctx_switch_opt() -> Option<(&InnerContext, InnerType)> { - Some((executor.context(), InnerType { a: 0 })) - } - - fn ctx_switch_res() -> FieldResult<(&InnerContext, InnerType)> { - Ok((executor.context(), InnerType { a: 0 })) - } - - fn ctx_switch_res_opt() -> FieldResult> { - Ok(Some((executor.context(), InnerType { a: 0 }))) - } -} - -struct Root; - -#[graphql_object(context = InnerContext)] -impl Root { - fn custom_name() -> CustomName { - CustomName {} - } - - fn with_lifetime() -> WithLifetime<'static> { - WithLifetime { data: PhantomData } - } - fn with_generics() -> WithGenerics { - WithGenerics { data: 123 } - } - - fn description_first() -> SimpleObject { - SimpleObject { simple: 0 } - } - fn interface() -> InterfaceValue { - SimpleObject { simple: 0 }.into() - } - - fn ctx_switcher() -> CtxSwitcher { - CtxSwitcher {} - } -} - -fn run_type_info_query(type_name: &str, f: F) -where - F: Fn(&Object, &Vec>) -> (), -{ - let doc = r#" - query ($typeName: String!) { - __type(name: $typeName) { - name - description - fields(includeDeprecated: true) { - name - type { - kind - name - ofType { - kind - name - } - } - } - interfaces { - name - kind - } - } - } - "#; - let schema = RootNode::new( - Root {}, - EmptyMutation::::new(), - EmptySubscription::::new(), - ); - let vars = vec![("typeName".to_owned(), InputValue::scalar(type_name))] - .into_iter() - .collect(); - - let (result, errs) = - crate::execute_sync(doc, None, &schema, &vars, &InnerContext).expect("Execution failed"); - - assert_eq!(errs, []); - - println!("Result: {:#?}", result); - - let type_info = result - .as_object_value() - .expect("Result is not an object") - .get_field_value("__type") - .expect("__type field missing") - .as_object_value() - .expect("__type field not an object value"); - - let fields = type_info - .get_field_value("fields") - .expect("fields field missing") - .as_list_value() - .expect("fields field not a list value"); - - f(type_info, fields); -} - -#[test] -fn introspect_custom_name() { - run_type_info_query("ACustomNamedType", |object, fields| { - assert_eq!( - object.get_field_value("name"), - Some(&Value::scalar("ACustomNamedType")) - ); - assert_eq!(object.get_field_value("description"), Some(&Value::null())); - assert_eq!( - object.get_field_value("interfaces"), - Some(&Value::list(vec![])) - ); - - assert!(fields.contains(&graphql_value!({ - "name": "simple", - "type": { - "kind": "NON_NULL", - "name": None, - "ofType": { "kind": "SCALAR", "name": "Int" } - } - }))); - }); -} - -#[test] -fn introspect_with_lifetime() { - run_type_info_query("WithLifetime", |object, fields| { - assert_eq!( - object.get_field_value("name"), - Some(&Value::scalar("WithLifetime")) - ); - assert_eq!(object.get_field_value("description"), Some(&Value::null())); - assert_eq!( - object.get_field_value("interfaces"), - Some(&Value::list(vec![])) - ); - - assert!(fields.contains(&graphql_value!({ - "name": "simple", - "type": { - "kind": "NON_NULL", "name": None, "ofType": { "kind": "SCALAR", "name": "Int" } - } - }))); - }); -} - -#[test] -fn introspect_with_generics() { - run_type_info_query("WithGenerics", |object, fields| { - assert_eq!( - object.get_field_value("name"), - Some(&Value::scalar("WithGenerics")) - ); - assert_eq!(object.get_field_value("description"), Some(&Value::null())); - assert_eq!( - object.get_field_value("interfaces"), - Some(&Value::list(vec![])) - ); - - assert!(fields.contains(&graphql_value!({ - "name": "simple", - "type": { - "kind": "NON_NULL", "name": None, "ofType": { "kind": "SCALAR", "name": "Int" } - } - }))); - }); -} - -#[test] -fn introspect_simple_object() { - run_type_info_query("SimpleObject", |object, fields| { - assert_eq!( - object.get_field_value("name"), - Some(&Value::scalar("SimpleObject")) - ); - assert_eq!( - object.get_field_value("description"), - Some(&Value::scalar("A description")) - ); - assert_eq!( - object.get_field_value("interfaces"), - Some(&Value::list(vec![Value::object( - vec![ - ("name", Value::scalar("Interface")), - ("kind", Value::scalar("INTERFACE")), - ] - .into_iter() - .collect(), - )])) - ); - - assert!(fields.contains(&graphql_value!({ - "name": "simple", - "type": { - "kind": "NON_NULL", "name": None, "ofType": { "kind": "SCALAR", "name": "Int" } - } - }))); - }); -} - -#[test] -fn introspect_ctx_switch() { - run_type_info_query("CtxSwitcher", |_, fields| { - assert!(fields.contains(&graphql_value!({ - "name": "ctxSwitchAlways", - "type": { - "kind": "NON_NULL", - "name": None, - "ofType": { - "kind": "OBJECT", - "name": "InnerType", - } - } - }))); - - assert!(fields.contains(&graphql_value!({ - "name": "ctxSwitchOpt", - "type": { - "kind": "OBJECT", - "name": "InnerType", - "ofType": None - } - }))); - - assert!(fields.contains(&graphql_value!({ - "name": "ctxSwitchRes", - "type": { - "kind": "NON_NULL", - "name": None, - "ofType": { - "kind": "OBJECT", - "name": "InnerType", - } - } - }))); - - assert!(fields.contains(&graphql_value!({ - "name": "ctxSwitchResOpt", - "type": { - "kind": "OBJECT", - "name": "InnerType", - "ofType": None - } - }))); - }); -} diff --git a/juniper/src/macros/tests/union.rs b/juniper/src/macros/tests/union.rs deleted file mode 100644 index f36010b65..000000000 --- a/juniper/src/macros/tests/union.rs +++ /dev/null @@ -1,219 +0,0 @@ -/* - -Syntax to validate: - -* Order of items: description, instance resolvers -* Optional Generics/lifetimes -* Custom name vs. default name -* Optional commas between items -* Optional trailing commas on instance resolvers -* -*/ - -use std::marker::PhantomData; - -use crate::{ - ast::InputValue, - graphql_object, - schema::model::RootNode, - types::scalars::{EmptyMutation, EmptySubscription}, - value::{DefaultScalarValue, Object, Value}, - GraphQLUnion, -}; - -struct Concrete; - -#[graphql_object] -impl Concrete { - fn simple() -> i32 { - 123 - } -} - -#[derive(GraphQLUnion)] -#[graphql(name = "ACustomNamedUnion")] -enum CustomName { - Concrete(Concrete), -} - -#[derive(GraphQLUnion)] -#[graphql(on Concrete = WithLifetime::resolve)] -enum WithLifetime<'a> { - #[graphql(ignore)] - Int(PhantomData<&'a i32>), -} - -impl<'a> WithLifetime<'a> { - fn resolve(&self, _: &()) -> Option<&Concrete> { - if matches!(self, Self::Int(_)) { - Some(&Concrete) - } else { - None - } - } -} - -#[derive(GraphQLUnion)] -#[graphql(on Concrete = WithGenerics::resolve)] -enum WithGenerics { - #[graphql(ignore)] - Generic(T), -} - -impl WithGenerics { - fn resolve(&self, _: &()) -> Option<&Concrete> { - if matches!(self, Self::Generic(_)) { - Some(&Concrete) - } else { - None - } - } -} - -#[derive(GraphQLUnion)] -#[graphql(description = "A description")] -enum DescriptionFirst { - Concrete(Concrete), -} - -struct Root; - -#[graphql_object] -impl Root { - fn custom_name() -> CustomName { - CustomName::Concrete(Concrete) - } - fn with_lifetime() -> WithLifetime<'_> { - WithLifetime::Int(PhantomData) - } - fn with_generics() -> WithGenerics { - WithGenerics::Generic(123) - } - fn description_first() -> DescriptionFirst { - DescriptionFirst::Concrete(Concrete) - } -} - -async fn run_type_info_query(type_name: &str, f: F) -where - F: Fn(&Object, &Vec>) -> (), -{ - let doc = r#" - query ($typeName: String!) { - __type(name: $typeName) { - name - description - possibleTypes { - name - } - } - } - "#; - let schema = RootNode::new( - Root {}, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); - let vars = vec![("typeName".to_owned(), InputValue::scalar(type_name))] - .into_iter() - .collect(); - - let (result, errs) = crate::execute(doc, None, &schema, &vars, &()) - .await - .expect("Execution failed"); - - assert_eq!(errs, []); - - println!("Result: {:#?}", result); - - let type_info = result - .as_object_value() - .expect("Result is not an object") - .get_field_value("__type") - .expect("__type field missing") - .as_object_value() - .expect("__type field not an object value"); - - let possible_types = type_info - .get_field_value("possibleTypes") - .expect("possibleTypes field missing") - .as_list_value() - .expect("possibleTypes field not a list value"); - - f(type_info, possible_types); -} - -#[tokio::test] -async fn introspect_custom_name() { - run_type_info_query("ACustomNamedUnion", |union, possible_types| { - assert_eq!( - union.get_field_value("name"), - Some(&Value::scalar("ACustomNamedUnion")) - ); - assert_eq!(union.get_field_value("description"), Some(&Value::null())); - - assert!(possible_types.contains(&Value::object( - vec![("name", Value::scalar("Concrete"))] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_with_lifetime() { - run_type_info_query("WithLifetime", |union, possible_types| { - assert_eq!( - union.get_field_value("name"), - Some(&Value::scalar("WithLifetime")) - ); - assert_eq!(union.get_field_value("description"), Some(&Value::null())); - - assert!(possible_types.contains(&Value::object( - vec![("name", Value::scalar("Concrete"))] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_with_generics() { - run_type_info_query("WithGenerics", |union, possible_types| { - assert_eq!( - union.get_field_value("name"), - Some(&Value::scalar("WithGenerics")) - ); - assert_eq!(union.get_field_value("description"), Some(&Value::null())); - - assert!(possible_types.contains(&Value::object( - vec![("name", Value::scalar("Concrete"))] - .into_iter() - .collect(), - ))); - }) - .await; -} - -#[tokio::test] -async fn introspect_description_first() { - run_type_info_query("DescriptionFirst", |union, possible_types| { - assert_eq!( - union.get_field_value("name"), - Some(&Value::scalar("DescriptionFirst")) - ); - assert_eq!( - union.get_field_value("description"), - Some(&Value::scalar("A description")) - ); - - assert!(possible_types.contains(&Value::object( - vec![("name", Value::scalar("Concrete"))] - .into_iter() - .collect(), - ))); - }) - .await; -} diff --git a/juniper/src/macros/tests/util.rs b/juniper/src/macros/tests/util.rs deleted file mode 100644 index 42ccadd4c..000000000 --- a/juniper/src/macros/tests/util.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::{DefaultScalarValue, GraphQLType, GraphQLTypeAsync, RootNode, Value, Variables}; - -pub async fn run_query(query: &str) -> Value -where - Query: GraphQLTypeAsync + Default, - Query::Context: Default + Sync, - Mutation: - GraphQLTypeAsync + Default, - Subscription: - GraphQLType + Default + Sync, -{ - let schema = RootNode::new( - Query::default(), - Mutation::default(), - Subscription::default(), - ); - let (result, errs) = crate::execute( - query, - None, - &schema, - &Variables::new(), - &Query::Context::default(), - ) - .await - .expect("Execution failed"); - - assert_eq!(errs, []); - result -} - -pub async fn run_info_query(type_name: &str) -> Value -where - Query: GraphQLTypeAsync + Default, - Query::Context: Default + Sync, - Mutation: - GraphQLTypeAsync + Default, - Subscription: - GraphQLType + Default + Sync, -{ - let query = format!( - r#" - {{ - __type(name: "{}") {{ - name, - description, - fields {{ - name - description - args {{ - name - description - type {{ - name - }} - }} - }} - }} - }} - "#, - type_name - ); - let result = run_query::(&query).await; - result - .as_object_value() - .expect("Result is not an object") - .get_field_value("__type") - .expect("__type field missing") - .clone() -} diff --git a/juniper/src/parser/tests/value.rs b/juniper/src/parser/tests/value.rs index fe425cd1e..c8b1ac9b9 100644 --- a/juniper/src/parser/tests/value.rs +++ b/juniper/src/parser/tests/value.rs @@ -30,11 +30,8 @@ struct Foo { struct Query; -#[crate::graphql_object(Scalar = S)] -impl<'a, S> Query -where - S: crate::ScalarValue + 'a, -{ +#[crate::graphql_object] +impl Query { fn int_field() -> i32 { 42 } diff --git a/juniper/src/schema/meta.rs b/juniper/src/schema/meta.rs index beb8e0ac7..f5ac2c0f7 100644 --- a/juniper/src/schema/meta.rs +++ b/juniper/src/schema/meta.rs @@ -32,10 +32,10 @@ impl DeprecationStatus { } /// An optional reason for the deprecation, or none if `Current`. - pub fn reason(&self) -> Option<&String> { + pub fn reason(&self) -> Option<&str> { match self { DeprecationStatus::Current => None, - DeprecationStatus::Deprecated(ref reason) => reason.as_ref(), + DeprecationStatus::Deprecated(rsn) => rsn.as_deref(), } } } @@ -236,26 +236,14 @@ impl<'a, S> MetaType<'a, S> { /// Access the description of the type, if applicable /// /// Lists, nullable wrappers, and placeholders don't have names. - pub fn description(&self) -> Option<&String> { - match *self { - MetaType::Scalar(ScalarMeta { - ref description, .. - }) - | MetaType::Object(ObjectMeta { - ref description, .. - }) - | MetaType::Enum(EnumMeta { - ref description, .. - }) - | MetaType::Interface(InterfaceMeta { - ref description, .. - }) - | MetaType::Union(UnionMeta { - ref description, .. - }) - | MetaType::InputObject(InputObjectMeta { - ref description, .. - }) => description.as_ref(), + pub fn description(&self) -> Option<&str> { + match self { + MetaType::Scalar(ScalarMeta { description, .. }) + | MetaType::Object(ObjectMeta { description, .. }) + | MetaType::Enum(EnumMeta { description, .. }) + | MetaType::Interface(InterfaceMeta { description, .. }) + | MetaType::Union(UnionMeta { description, .. }) + | MetaType::InputObject(InputObjectMeta { description, .. }) => description.as_deref(), _ => None, } } @@ -670,7 +658,7 @@ impl<'a, S> Field<'a, S> { impl<'a, S> Argument<'a, S> { #[doc(hidden)] pub fn new(name: &str, arg_type: Type<'a>) -> Self { - Argument { + Self { name: name.to_owned(), description: None, arg_type, diff --git a/juniper/src/schema/model.rs b/juniper/src/schema/model.rs index b0516fe04..953904cb0 100644 --- a/juniper/src/schema/model.rs +++ b/juniper/src/schema/model.rs @@ -640,12 +640,8 @@ mod test { fn whatever() -> String { "foo".to_string() } - fn arr(stuff: Vec) -> Option<&str> { - if stuff.is_empty() { - None - } else { - Some("stuff") - } + fn arr(stuff: Vec) -> Option<&'static str> { + (!stuff.is_empty()).then(|| "stuff") } fn fruit() -> Fruit { Fruit::Apple diff --git a/juniper/src/schema/schema.rs b/juniper/src/schema/schema.rs index a877afe78..5651ccff5 100644 --- a/juniper/src/schema/schema.rs +++ b/juniper/src/schema/schema.rs @@ -1,6 +1,7 @@ use crate::{ ast::Selection, executor::{ExecutionResult, Executor, Registry}, + graphql_object, types::{ async_await::{GraphQLTypeAsync, GraphQLValueAsync}, base::{Arguments, GraphQLType, GraphQLValue, TypeKind}, @@ -129,18 +130,13 @@ where } } -#[crate::graphql_object( +#[graphql_object( name = "__Schema" - Context = SchemaType<'a, S>, - Scalar = S, + context = SchemaType<'a, S>, + scalar = S, internal, - // FIXME: make this redundant. - noasync, )] -impl<'a, S> SchemaType<'a, S> -where - S: crate::ScalarValue + 'a, -{ +impl<'a, S: ScalarValue + 'a> SchemaType<'a, S> { fn types(&self) -> Vec> { self.type_list() .into_iter() @@ -152,18 +148,21 @@ where }) .unwrap_or(false) }) - .collect::>() + .collect() } - fn query_type(&self) -> TypeType { + #[graphql(name = "queryType")] + fn query_type_(&self) -> TypeType { self.query_type() } - fn mutation_type(&self) -> Option> { + #[graphql(name = "mutationType")] + fn mutation_type_(&self) -> Option> { self.mutation_type() } - fn subscription_type(&self) -> Option> { + #[graphql(name = "subscriptionType")] + fn subscription_type_(&self) -> Option> { self.subscription_type() } @@ -172,43 +171,37 @@ where } } -#[crate::graphql_object( +#[graphql_object( name = "__Type" - Context = SchemaType<'a, S>, - Scalar = S, + context = SchemaType<'a, S>, + scalar = S, internal, - // FIXME: make this redundant. - noasync, )] -impl<'a, S> TypeType<'a, S> -where - S: crate::ScalarValue + 'a, -{ +impl<'a, S: ScalarValue + 'a> TypeType<'a, S> { fn name(&self) -> Option<&str> { - match *self { + match self { TypeType::Concrete(t) => t.name(), _ => None, } } - fn description(&self) -> Option<&String> { - match *self { + fn description(&self) -> Option<&str> { + match self { TypeType::Concrete(t) => t.description(), _ => None, } } fn kind(&self) -> TypeKind { - match *self { + match self { TypeType::Concrete(t) => t.type_kind(), TypeType::List(..) => TypeKind::List, TypeType::NonNull(_) => TypeKind::NonNull, } } - #[graphql(arguments(include_deprecated(default = false)))] - fn fields(&self, include_deprecated: bool) -> Option>> { - match *self { + fn fields(&self, #[graphql(default)] include_deprecated: bool) -> Option>> { + match self { TypeType::Concrete(&MetaType::Interface(InterfaceMeta { ref fields, .. })) | TypeType::Concrete(&MetaType::Object(ObjectMeta { ref fields, .. })) => Some( fields @@ -221,53 +214,53 @@ where } } - fn of_type(&self) -> Option<&Box>> { - match *self { + fn of_type(&self) -> Option<&TypeType> { + match self { TypeType::Concrete(_) => None, - TypeType::List(ref l, _) | TypeType::NonNull(ref l) => Some(l), + TypeType::List(l, _) | TypeType::NonNull(l) => Some(&*l), } } - fn input_fields(&self) -> Option<&Vec>> { - match *self { + fn input_fields(&self) -> Option<&[Argument]> { + match self { TypeType::Concrete(&MetaType::InputObject(InputObjectMeta { ref input_fields, .. - })) => Some(input_fields), + })) => Some(input_fields.as_slice()), _ => None, } } - fn interfaces(&self, schema: &SchemaType<'a, S>) -> Option>> { - match *self { + fn interfaces<'s>(&self, context: &'s SchemaType<'a, S>) -> Option>> { + match self { TypeType::Concrete(&MetaType::Object(ObjectMeta { ref interface_names, .. })) => Some( interface_names .iter() - .filter_map(|n| schema.type_by_name(n)) + .filter_map(|n| context.type_by_name(n)) .collect(), ), _ => None, } } - fn possible_types(&self, schema: &SchemaType<'a, S>) -> Option>> { - match *self { + fn possible_types<'s>(&self, context: &'s SchemaType<'a, S>) -> Option>> { + match self { TypeType::Concrete(&MetaType::Union(UnionMeta { ref of_type_names, .. })) => Some( of_type_names .iter() - .filter_map(|tn| schema.type_by_name(tn)) + .filter_map(|tn| context.type_by_name(tn)) .collect(), ), TypeType::Concrete(&MetaType::Interface(InterfaceMeta { name: ref iface_name, .. })) => Some( - schema + context .concrete_type_list() .iter() .filter_map(|&ct| { @@ -278,7 +271,7 @@ where }) = *ct { if interface_names.contains(&iface_name.to_string()) { - schema.type_by_name(name) + context.type_by_name(name) } else { None } @@ -292,9 +285,8 @@ where } } - #[graphql(arguments(include_deprecated(default = false)))] - fn enum_values(&self, include_deprecated: bool) -> Option> { - match *self { + fn enum_values(&self, #[graphql(default)] include_deprecated: bool) -> Option> { + match self { TypeType::Concrete(&MetaType::Enum(EnumMeta { ref values, .. })) => Some( values .iter() @@ -306,24 +298,20 @@ where } } -#[crate::graphql_object( +#[graphql_object( name = "__Field", - Context = SchemaType<'a, S>, - Scalar = S, + context = SchemaType<'a, S>, + scalar = S, internal, - // FIXME: make this redundant. - noasync, )] -impl<'a, S> Field<'a, S> -where - S: crate::ScalarValue + 'a, -{ +impl<'a, S: ScalarValue + 'a> Field<'a, S> { fn name(&self) -> String { self.name.clone().into() } - fn description(&self) -> &Option { - &self.description + #[graphql(name = "description")] + fn description_(&self) -> Option<&str> { + self.description.as_deref() } fn args(&self) -> Vec<&Argument> { @@ -333,7 +321,7 @@ where } #[graphql(name = "type")] - fn _type(&self, context: &SchemaType<'a, S>) -> TypeType { + fn type_<'s>(&self, context: &'s SchemaType<'a, S>) -> TypeType<'s, S> { context.make_type(&self.field_type) } @@ -341,94 +329,79 @@ where self.deprecation_status.is_deprecated() } - fn deprecation_reason(&self) -> Option<&String> { + fn deprecation_reason(&self) -> Option<&str> { self.deprecation_status.reason() } } -#[crate::graphql_object( +#[graphql_object( name = "__InputValue", - Context = SchemaType<'a, S>, - Scalar = S, + context = SchemaType<'a, S>, + scalar = S, internal, - // FIXME: make this redundant. - noasync, )] -impl<'a, S> Argument<'a, S> -where - S: crate::ScalarValue + 'a, -{ - fn name(&self) -> &String { +impl<'a, S: ScalarValue + 'a> Argument<'a, S> { + fn name(&self) -> &str { &self.name } - fn description(&self) -> &Option { - &self.description + #[graphql(name = "description")] + fn description_(&self) -> Option<&str> { + self.description.as_deref() } #[graphql(name = "type")] - fn _type(&self, context: &SchemaType<'a, S>) -> TypeType { + fn type_<'s>(&self, context: &'s SchemaType<'a, S>) -> TypeType<'s, S> { context.make_type(&self.arg_type) } - fn default_value(&self) -> Option { - self.default_value.as_ref().map(|v| format!("{}", v)) + #[graphql(name = "defaultValue")] + fn default_value_(&self) -> Option { + self.default_value.as_ref().map(ToString::to_string) } } -#[crate::graphql_object( - name = "__EnumValue", - Scalar = S, - internal, - // FIXME: make this redundant. - noasync, -)] -impl<'a, S> EnumValue -where - S: crate::ScalarValue + 'a, -{ - fn name(&self) -> &String { +#[graphql_object(name = "__EnumValue", internal)] +impl EnumValue { + fn name(&self) -> &str { &self.name } - fn description(&self) -> &Option { - &self.description + #[graphql(name = "description")] + fn description_(&self) -> Option<&str> { + self.description.as_deref() } fn is_deprecated(&self) -> bool { self.deprecation_status.is_deprecated() } - fn deprecation_reason(&self) -> Option<&String> { + fn deprecation_reason(&self) -> Option<&str> { self.deprecation_status.reason() } } -#[crate::graphql_object( +#[graphql_object( name = "__Directive", - Context = SchemaType<'a, S>, - Scalar = S, + context = SchemaType<'a, S>, + scalar = S, internal, - // FIXME: make this redundant. - noasync, )] -impl<'a, S> DirectiveType<'a, S> -where - S: crate::ScalarValue + 'a, -{ - fn name(&self) -> &String { +impl<'a, S: ScalarValue + 'a> DirectiveType<'a, S> { + fn name(&self) -> &str { &self.name } - fn description(&self) -> &Option { - &self.description + #[graphql(name = "description")] + fn description_(&self) -> Option<&str> { + self.description.as_deref() } - fn locations(&self) -> &Vec { + fn locations(&self) -> &[DirectiveLocation] { &self.locations } - fn args(&self) -> &Vec> { + fn args(&self) -> &[Argument] { &self.arguments } diff --git a/juniper/src/tests/fixtures/starwars/schema.rs b/juniper/src/tests/fixtures/starwars/schema.rs index 4b84568cb..3b220bdd5 100644 --- a/juniper/src/tests/fixtures/starwars/schema.rs +++ b/juniper/src/tests/fixtures/starwars/schema.rs @@ -9,21 +9,26 @@ pub struct Query; #[graphql_object(context = Database)] /// The root query object of the schema impl Query { - #[graphql(arguments(id(description = "id of the human")))] - fn human(database: &Database, id: String) -> Option<&Human> { + fn human( + #[graphql(context)] database: &Database, + #[graphql(description = "id of the human")] id: String, + ) -> Option<&Human> { database.get_human(&id) } - #[graphql(arguments(id(description = "id of the droid")))] - fn droid(database: &Database, id: String) -> Option<&Droid> { + fn droid( + #[graphql(context)] database: &Database, + #[graphql(description = "id of the droid")] id: String, + ) -> Option<&Droid> { database.get_droid(&id) } - #[graphql(arguments(episode( - description = "If omitted, returns the hero of the whole saga. \ - If provided, returns the hero of that particular episode" - )))] - fn hero(database: &Database, episode: Option) -> Option { + fn hero( + #[graphql(context)] database: &Database, + #[graphql(description = "If omitted, returns the hero of the whole saga. \ + If provided, returns the hero of that particular episode")] + episode: Option, + ) -> Option { Some(database.get_hero(episode)) } } diff --git a/juniper/src/tests/subscriptions.rs b/juniper/src/tests/subscriptions.rs index 66d118fcb..ac61d504e 100644 --- a/juniper/src/tests/subscriptions.rs +++ b/juniper/src/tests/subscriptions.rs @@ -1,10 +1,10 @@ use std::{iter, iter::FromIterator as _, pin::Pin}; -use futures::{self, StreamExt as _}; +use futures::{stream, StreamExt as _}; use crate::{ - http::GraphQLRequest, Context, DefaultScalarValue, EmptyMutation, ExecutionError, FieldError, - GraphQLObject, Object, RootNode, Value, + graphql_object, graphql_subscription, http::GraphQLRequest, Context, DefaultScalarValue, + EmptyMutation, ExecutionError, FieldError, GraphQLObject, Object, RootNode, Value, }; #[derive(Debug, Clone)] @@ -22,7 +22,7 @@ struct Human { struct MyQuery; -#[crate::graphql_object(context = MyContext)] +#[graphql_object(context = MyContext)] impl MyQuery { fn test(&self) -> i32 { 0 // NOTICE: does not serve a purpose @@ -42,10 +42,10 @@ type HumanStream = Pin + Send>>; struct MySubscription; -#[crate::graphql_subscription(context = MyContext)] +#[graphql_subscription(context = MyContext)] impl MySubscription { async fn async_human() -> HumanStream { - Box::pin(futures::stream::once(async { + Box::pin(stream::once(async { Human { id: "stream id".to_string(), name: "stream name".to_string(), @@ -61,9 +61,9 @@ impl MySubscription { )) } - async fn human_with_context(ctxt: &MyContext) -> HumanStream { - let context_val = ctxt.0.clone(); - Box::pin(futures::stream::once(async move { + async fn human_with_context(context: &MyContext) -> HumanStream { + let context_val = context.0.clone(); + Box::pin(stream::once(async move { Human { id: context_val.to_string(), name: context_val.to_string(), @@ -73,7 +73,7 @@ impl MySubscription { } async fn human_with_args(id: String, name: String) -> HumanStream { - Box::pin(futures::stream::once(async { + Box::pin(stream::once(async { Human { id, name, diff --git a/juniper/src/types/base.rs b/juniper/src/types/base.rs index 1b351a215..60979a409 100644 --- a/juniper/src/types/base.rs +++ b/juniper/src/types/base.rs @@ -105,7 +105,7 @@ where /// the `InputValue` will be converted into the type `T`. /// /// Returns `Some` if the argument is present _and_ type conversion - /// succeeeds. + /// succeeds. pub fn get(&self, key: &str) -> Option where T: FromInputValue, @@ -384,6 +384,8 @@ pub type DynGraphQLValue = /// } /// } /// ``` +/// +/// [3]: https://spec.graphql.org/June2018/#sec-Objects pub trait GraphQLType: GraphQLValue where S: ScalarValue, diff --git a/juniper/src/types/marker.rs b/juniper/src/types/marker.rs index 7c07f4aae..b04a41906 100644 --- a/juniper/src/types/marker.rs +++ b/juniper/src/types/marker.rs @@ -9,25 +9,33 @@ use std::sync::Arc; use crate::{GraphQLType, ScalarValue}; -/// Maker object for GraphQL objects. +/// Maker trait for [GraphQL objects][1]. /// -/// This trait extends the GraphQLType and is only used to mark -/// object. During compile this addition information is required to -/// prevent unwanted structure compiling. If an object requires this -/// trait instead of the GraphQLType, then it explicitly requires an -/// GraphQL objects. Other types (scalars, enums, and input objects) -/// are not allowed. -pub trait GraphQLObjectType: GraphQLType { +/// This trait extends the [`GraphQLType`] and is only used to mark an [object][1]. During +/// compile this addition information is required to prevent unwanted structure compiling. If an +/// object requires this trait instead of the [`GraphQLType`], then it explicitly requires +/// [GraphQL objects][1]. Other types ([scalars][2], [enums][3], [interfaces][4], [input objects][5] +/// and [unions][6]) are not allowed. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Objects +/// [2]: https://spec.graphql.org/June2018/#sec-Scalars +/// [3]: https://spec.graphql.org/June2018/#sec-Enums +/// [4]: https://spec.graphql.org/June2018/#sec-Interfaces +/// [5]: https://spec.graphql.org/June2018/#sec-Input-Objects +/// [6]: https://spec.graphql.org/June2018/#sec-Unions +pub trait GraphQLObject: GraphQLType { /// An arbitrary function without meaning. /// - /// May contain compile timed check logic which ensures that types - /// are used correctly according to the GraphQL specification. + /// May contain compile timed check logic which ensures that types are used correctly according + /// to the [GraphQL specification][1]. + /// + /// [1]: https://spec.graphql.org/June2018/ fn mark() {} } -impl<'a, S, T> GraphQLObjectType for &T +impl<'a, S, T> GraphQLObject for &T where - T: GraphQLObjectType + ?Sized, + T: GraphQLObject + ?Sized, S: ScalarValue, { #[inline] @@ -36,9 +44,9 @@ where } } -impl GraphQLObjectType for Box +impl GraphQLObject for Box where - T: GraphQLObjectType + ?Sized, + T: GraphQLObject + ?Sized, S: ScalarValue, { #[inline] @@ -47,9 +55,9 @@ where } } -impl GraphQLObjectType for Arc +impl GraphQLObject for Arc where - T: GraphQLObjectType + ?Sized, + T: GraphQLObject + ?Sized, S: ScalarValue, { #[inline] diff --git a/juniper/src/types/nullable.rs b/juniper/src/types/nullable.rs index 0e3659fce..e463f74d2 100644 --- a/juniper/src/types/nullable.rs +++ b/juniper/src/types/nullable.rs @@ -290,8 +290,8 @@ where } } - fn from_implicit_null() -> Self { - Self::ImplicitNull + fn from_implicit_null() -> Option { + Some(Self::ImplicitNull) } } diff --git a/juniper/src/types/subscriptions.rs b/juniper/src/types/subscriptions.rs index 2b5981e4d..a6c28fa75 100644 --- a/juniper/src/types/subscriptions.rs +++ b/juniper/src/types/subscriptions.rs @@ -82,6 +82,8 @@ where /// /// It can be treated as [`futures::Stream`] yielding [`GraphQLResponse`]s in /// server integration crates. +/// +/// [`GraphQLResponse`]: crate::http::GraphQLResponse pub trait SubscriptionConnection: futures::Stream> {} /// Extension of [`GraphQLValue`] trait with asynchronous [subscription][1] execution logic. diff --git a/juniper_actix/examples/actix_server.rs b/juniper_actix/examples/actix_server.rs index 7a9638075..38981a666 100644 --- a/juniper_actix/examples/actix_server.rs +++ b/juniper_actix/examples/actix_server.rs @@ -71,12 +71,15 @@ impl juniper::Context for Database {} struct Query; #[graphql_object(context = Database)] impl Query { - fn apiVersion() -> String { - "1.0".to_string() + fn api_version() -> &'static str { + "1.0" } - #[graphql(arguments(id(description = "id of the user")))] - fn user(database: &Database, id: i32) -> Option<&User> { - database.get_user(&id) + + fn user( + context: &Database, + #[graphql(description = "id of the user")] id: i32, + ) -> Option<&User> { + context.get_user(&id) } } diff --git a/juniper_codegen/src/common/field/arg.rs b/juniper_codegen/src/common/field/arg.rs new file mode 100644 index 000000000..0c6d16a94 --- /dev/null +++ b/juniper_codegen/src/common/field/arg.rs @@ -0,0 +1,452 @@ +//! Common functions, definitions and extensions for parsing and code generation +//! of [GraphQL arguments][1] +//! +//! [1]: https://spec.graphql.org/June2018/#sec-Language.Arguments. + +use std::mem; + +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + ext::IdentExt as _, + parse::{Parse, ParseStream}, + spanned::Spanned, + token, +}; + +use crate::{ + common::{ + parse::{ + attr::{err, OptionExt as _}, + ParseBufferExt as _, TypeExt as _, + }, + scalar, + }, + result::GraphQLScope, + util::{filter_attrs, path_eq_single, span_container::SpanContainer, RenameRule}, +}; + +/// Available metadata (arguments) behind `#[graphql]` attribute placed on a +/// method argument, when generating code for [GraphQL argument][1]. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Language.Arguments +#[derive(Debug, Default)] +pub(crate) struct Attr { + /// Explicitly specified name of a [GraphQL argument][1] represented by this + /// method argument. + /// + /// If [`None`], then `camelCased` Rust argument name is used by default. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Arguments + pub(crate) name: Option>, + + /// Explicitly specified [description][2] of this [GraphQL argument][1]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Arguments + /// [2]: https://spec.graphql.org/June2018/#sec-Descriptions + pub(crate) description: Option>, + + /// Explicitly specified [default value][2] of this [GraphQL argument][1]. + /// + /// If the exact default expression is not specified, then the [`Default`] + /// value is used. + /// + /// If [`None`], then this [GraphQL argument][1] is considered as + /// [required][2]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Arguments + /// [2]: https://spec.graphql.org/June2018/#sec-Required-Arguments + pub(crate) default: Option>>, + + /// Explicitly specified marker indicating that this method argument doesn't + /// represent a [GraphQL argument][1], but is a [`Context`] being injected + /// into a [GraphQL field][2] resolving function. + /// + /// If absent, then the method argument still is considered as [`Context`] + /// if it's named `context` or `ctx`. + /// + /// [`Context`]: juniper::Context + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Arguments + /// [2]: https://spec.graphql.org/June2018/#sec-Language.Fields + pub(crate) context: Option>, + + /// Explicitly specified marker indicating that this method argument doesn't + /// represent a [GraphQL argument][1], but is an [`Executor`] being injected + /// into a [GraphQL field][2] resolving function. + /// + /// If absent, then the method argument still is considered as [`Executor`] + /// if it's named `executor`. + /// + /// [`Executor`]: juniper::Executor + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Arguments + /// [2]: https://spec.graphql.org/June2018/#sec-Language.Fields + pub(crate) executor: Option>, +} + +impl Parse for Attr { + fn parse(input: ParseStream<'_>) -> syn::Result { + let mut out = Self::default(); + while !input.is_empty() { + let ident = input.parse::()?; + match ident.to_string().as_str() { + "name" => { + input.parse::()?; + let name = input.parse::()?; + out.name + .replace(SpanContainer::new(ident.span(), Some(name.span()), name)) + .none_or_else(|_| err::dup_arg(&ident))? + } + "desc" | "description" => { + input.parse::()?; + let desc = input.parse::()?; + out.description + .replace(SpanContainer::new(ident.span(), Some(desc.span()), desc)) + .none_or_else(|_| err::dup_arg(&ident))? + } + "default" => { + let mut expr = None; + if input.is_next::() { + input.parse::()?; + expr = Some(input.parse::()?); + } else if input.is_next::() { + let inner; + let _ = syn::parenthesized!(inner in input); + expr = Some(inner.parse::()?); + } + out.default + .replace(SpanContainer::new( + ident.span(), + expr.as_ref().map(|e| e.span()), + expr, + )) + .none_or_else(|_| err::dup_arg(&ident))? + } + "ctx" | "context" | "Context" => { + let span = ident.span(); + out.context + .replace(SpanContainer::new(span, Some(span), ident)) + .none_or_else(|_| err::dup_arg(span))? + } + "exec" | "executor" => { + let span = ident.span(); + out.executor + .replace(SpanContainer::new(span, Some(span), ident)) + .none_or_else(|_| err::dup_arg(span))? + } + name => { + return Err(err::unknown_arg(&ident, name)); + } + } + input.try_parse::()?; + } + Ok(out) + } +} + +impl Attr { + /// Tries to merge two [`Attr`]s into a single one, reporting about + /// duplicates, if any. + fn try_merge(self, mut another: Self) -> syn::Result { + Ok(Self { + name: try_merge_opt!(name: self, another), + description: try_merge_opt!(description: self, another), + default: try_merge_opt!(default: self, another), + context: try_merge_opt!(context: self, another), + executor: try_merge_opt!(executor: self, another), + }) + } + + /// Parses [`Attr`] from the given multiple `name`d [`syn::Attribute`]s + /// placed on a function argument. + pub(crate) fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result { + let attr = filter_attrs(name, attrs) + .map(|attr| attr.parse_args()) + .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?; + + if let Some(context) = &attr.context { + if attr.name.is_some() + || attr.description.is_some() + || attr.default.is_some() + || attr.executor.is_some() + { + return Err(syn::Error::new( + context.span(), + "`context` attribute argument is not composable with any other arguments", + )); + } + } + + if let Some(executor) = &attr.executor { + if attr.name.is_some() + || attr.description.is_some() + || attr.default.is_some() + || attr.context.is_some() + { + return Err(syn::Error::new( + executor.span(), + "`executor` attribute argument is not composable with any other arguments", + )); + } + } + + Ok(attr) + } + + /// Checks whether this [`Attr`] doesn't contain arguments related to an + /// [`OnField`] argument. + #[must_use] + fn ensure_no_regular_arguments(&self) -> syn::Result<()> { + if let Some(span) = &self.name { + return Err(Self::err_disallowed(&span, "name")); + } + if let Some(span) = &self.description { + return Err(Self::err_disallowed(&span, "description")); + } + if let Some(span) = &self.default { + return Err(Self::err_disallowed(&span, "default")); + } + Ok(()) + } + + /// Emits "argument is not allowed" [`syn::Error`] for the given `arg` + /// pointing to the given `span`. + #[must_use] + fn err_disallowed(span: &S, arg: &str) -> syn::Error { + syn::Error::new( + span.span(), + format!( + "attribute argument `#[graphql({} = ...)]` is not allowed here", + arg, + ), + ) + } +} + +/// Representation of a [GraphQL field argument][1] for code generation. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Language.Arguments +#[derive(Debug)] +pub(crate) struct OnField { + /// Rust type that this [GraphQL field argument][1] is represented by. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Arguments + pub(crate) ty: syn::Type, + + /// Name of this [GraphQL field argument][2] in GraphQL schema. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Arguments + pub(crate) name: String, + + /// [Description][2] of this [GraphQL field argument][1] to put into GraphQL + /// schema. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Arguments + /// [2]: https://spec.graphql.org/June2018/#sec-Descriptions + pub(crate) description: Option, + + /// Default value of this [GraphQL field argument][1] in GraphQL schema. + /// + /// If outer [`Option`] is [`None`], then this [argument][1] is a + /// [required][2] one. + /// + /// If inner [`Option`] is [`None`], then the [`Default`] value is used. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Arguments + /// [2]: https://spec.graphql.org/June2018/#sec-Required-Arguments + pub(crate) default: Option>, +} + +/// Possible kinds of Rust method arguments for code generation. +#[derive(Debug)] +pub(crate) enum OnMethod { + /// Regular [GraphQL field argument][1]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Arguments + Regular(OnField), + + /// [`Context`] passed into a [GraphQL field][2] resolving method. + /// + /// [`Context`]: juniper::Context + /// [2]: https://spec.graphql.org/June2018/#sec-Language.Fields + Context(syn::Type), + + /// [`Executor`] passed into a [GraphQL field][2] resolving method. + /// + /// [`Executor`]: juniper::Executor + /// [2]: https://spec.graphql.org/June2018/#sec-Language.Fields + Executor, +} + +impl OnMethod { + /// Returns this argument as the one [`OnField`], if it represents the one. + #[must_use] + pub(crate) fn as_regular(&self) -> Option<&OnField> { + if let Self::Regular(arg) = self { + Some(arg) + } else { + None + } + } + + /// Returns [`syn::Type`] of this [`OnMethod::Context`], if it represents + /// the one. + #[must_use] + pub(crate) fn context_ty(&self) -> Option<&syn::Type> { + if let Self::Context(ty) = self { + Some(ty) + } else { + None + } + } + + /// Returns generated code for the [`marker::IsOutputType::mark`] method, + /// which performs static checks for this argument, if it represents an + /// [`OnField`] one. + /// + /// [`marker::IsOutputType::mark`]: juniper::marker::IsOutputType::mark + #[must_use] + pub(crate) fn method_mark_tokens(&self, scalar: &scalar::Type) -> Option { + let ty = &self.as_regular()?.ty; + Some(quote! { + <#ty as ::juniper::marker::IsInputType<#scalar>>::mark(); + }) + } + + /// Returns generated code for the [`GraphQLType::meta`] method, which + /// registers this argument in [`Registry`], if it represents an [`OnField`] + /// argument. + /// + /// [`GraphQLType::meta`]: juniper::GraphQLType::meta + /// [`Registry`]: juniper::Registry + #[must_use] + pub(crate) fn method_meta_tokens(&self) -> Option { + let arg = self.as_regular()?; + + let (name, ty) = (&arg.name, &arg.ty); + + let description = arg + .description + .as_ref() + .map(|desc| quote! { .description(#desc) }); + + let method = if let Some(val) = &arg.default { + let val = val + .as_ref() + .map(|v| quote! { (#v).into() }) + .unwrap_or_else(|| quote! { <#ty as Default>::default() }); + quote! { .arg_with_default::<#ty>(#name, &#val, info) } + } else { + quote! { .arg::<#ty>(#name, info) } + }; + + Some(quote! { .argument(registry#method#description) }) + } + + /// Returns generated code for the [`GraphQLValue::resolve_field`] method, + /// which provides the value of this [`OnMethod`] argument to be passed into + /// a trait method call. + /// + /// [`GraphQLValue::resolve_field`]: juniper::GraphQLValue::resolve_field + #[must_use] + pub(crate) fn method_resolve_field_tokens(&self, scalar: &scalar::Type) -> TokenStream { + match self { + Self::Regular(arg) => { + let (name, ty) = (&arg.name, &arg.ty); + let err_text = format!( + "Internal error: missing argument `{}` - validation must have failed", + &name, + ); + quote! { + args.get::<#ty>(#name) + .or_else(::juniper::FromInputValue::<#scalar>::from_implicit_null) + .expect(#err_text) + } + } + + Self::Context(_) => quote! { + ::juniper::FromContext::from(executor.context()) + }, + + Self::Executor => quote! { &executor }, + } + } + + /// Parses an [`OnMethod`] argument from the given Rust method argument + /// definition. + /// + /// Returns [`None`] if parsing fails and emits parsing errors into the + /// given `scope`. + pub(crate) fn parse( + argument: &mut syn::PatType, + renaming: &RenameRule, + scope: &GraphQLScope, + ) -> Option { + let orig_attrs = argument.attrs.clone(); + + // Remove repeated attributes from the method, to omit incorrect expansion. + argument.attrs = mem::take(&mut argument.attrs) + .into_iter() + .filter(|attr| !path_eq_single(&attr.path, "graphql")) + .collect(); + + let attr = Attr::from_attrs("graphql", &orig_attrs) + .map_err(|e| proc_macro_error::emit_error!(e)) + .ok()?; + + if attr.context.is_some() { + return Some(Self::Context(argument.ty.unreferenced().clone())); + } + if attr.executor.is_some() { + return Some(Self::Executor); + } + if let syn::Pat::Ident(name) = &*argument.pat { + let arg = match name.ident.unraw().to_string().as_str() { + "context" | "ctx" | "_context" | "_ctx" => { + Some(Self::Context(argument.ty.unreferenced().clone())) + } + "executor" | "_executor" => Some(Self::Executor), + _ => None, + }; + if arg.is_some() { + attr.ensure_no_regular_arguments() + .map_err(|e| scope.error(e).emit()) + .ok()?; + return arg; + } + } + + let name = if let Some(name) = attr.name.as_ref() { + name.as_ref().value() + } else if let syn::Pat::Ident(name) = &*argument.pat { + renaming.apply(&name.ident.unraw().to_string()) + } else { + scope + .custom( + argument.pat.span(), + "method argument should be declared as a single identifier", + ) + .note(String::from( + "use `#[graphql(name = ...)]` attribute to specify custom argument's \ + name without requiring it being a single identifier", + )) + .emit(); + return None; + }; + if name.starts_with("__") { + scope.no_double_underscore( + attr.name + .as_ref() + .map(SpanContainer::span_ident) + .unwrap_or_else(|| argument.pat.span()), + ); + return None; + } + + Some(Self::Regular(OnField { + name, + ty: argument.ty.as_ref().clone(), + description: attr.description.as_ref().map(|d| d.as_ref().value()), + default: attr.default.as_ref().map(|v| v.as_ref().clone()), + })) + } +} diff --git a/juniper_codegen/src/common/field/mod.rs b/juniper_codegen/src/common/field/mod.rs new file mode 100644 index 000000000..a627e444b --- /dev/null +++ b/juniper_codegen/src/common/field/mod.rs @@ -0,0 +1,563 @@ +//! Common functions, definitions and extensions for parsing and code generation +//! of [GraphQL fields][1] +//! +//! [1]: https://spec.graphql.org/June2018/#sec-Language.Fields. + +pub(crate) mod arg; + +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + parse_quote, + spanned::Spanned as _, + token, +}; + +use crate::{ + common::{ + gen, + parse::{ + attr::{err, OptionExt as _}, + ParseBufferExt as _, + }, + scalar, + }, + util::{filter_attrs, get_deprecated, get_doc_comment, span_container::SpanContainer}, +}; + +pub(crate) use self::arg::OnMethod as MethodArgument; + +/// Available metadata (arguments) behind `#[graphql]` attribute placed on a +/// [GraphQL field][1] definition. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields +#[derive(Debug, Default)] +pub(crate) struct Attr { + /// Explicitly specified name of this [GraphQL field][1]. + /// + /// If [`None`], then `camelCased` Rust method name is used by default. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + pub(crate) name: Option>, + + /// Explicitly specified [description][2] of this [GraphQL field][1]. + /// + /// If [`None`], then Rust doc comment is used as the [description][2], if + /// any. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + /// [2]: https://spec.graphql.org/June2018/#sec-Descriptions + pub(crate) description: Option>, + + /// Explicitly specified [deprecation][2] of this [GraphQL field][1]. + /// + /// If [`None`], then Rust `#[deprecated]` attribute is used as the + /// [deprecation][2], if any. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + /// [2]: https://spec.graphql.org/June2018/#sec-Deprecation + pub(crate) deprecated: Option>>, + + /// Explicitly specified marker indicating that this method (or struct + /// field) should be omitted by code generation and not considered as the + /// [GraphQL field][1] definition. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + pub(crate) ignore: Option>, + + /// Explicitly specified marker indicating that this trait method doesn't + /// represent a [GraphQL field][1], but is a downcasting function into the + /// [GraphQL object][2] implementer type returned by this trait method. + /// + /// Once this marker is specified, the [GraphQL object][2] implementer type + /// cannot be downcast via another trait method or external downcasting + /// function. + /// + /// Omit using this field if you're generating code for [GraphQL object][2]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + /// [2]: https://spec.graphql.org/June2018/#sec-Objects + pub(crate) downcast: Option>, +} + +impl Parse for Attr { + fn parse(input: ParseStream<'_>) -> syn::Result { + let mut out = Self::default(); + while !input.is_empty() { + let ident = input.parse::()?; + match ident.to_string().as_str() { + "name" => { + input.parse::()?; + let name = input.parse::()?; + out.name + .replace(SpanContainer::new(ident.span(), Some(name.span()), name)) + .none_or_else(|_| err::dup_arg(&ident))? + } + "desc" | "description" => { + input.parse::()?; + let desc = input.parse::()?; + out.description + .replace(SpanContainer::new(ident.span(), Some(desc.span()), desc)) + .none_or_else(|_| err::dup_arg(&ident))? + } + "deprecated" => { + let mut reason = None; + if input.is_next::() { + input.parse::()?; + reason = Some(input.parse::()?); + } + out.deprecated + .replace(SpanContainer::new( + ident.span(), + reason.as_ref().map(|r| r.span()), + reason, + )) + .none_or_else(|_| err::dup_arg(&ident))? + } + "ignore" | "skip" => out + .ignore + .replace(SpanContainer::new(ident.span(), None, ident.clone())) + .none_or_else(|_| err::dup_arg(&ident))?, + "downcast" => out + .downcast + .replace(SpanContainer::new(ident.span(), None, ident.clone())) + .none_or_else(|_| err::dup_arg(&ident))?, + name => { + return Err(err::unknown_arg(&ident, name)); + } + } + input.try_parse::()?; + } + Ok(out) + } +} + +impl Attr { + /// Tries to merge two [`Attrs`]s into a single one, reporting about + /// duplicates, if any. + fn try_merge(self, mut another: Self) -> syn::Result { + Ok(Self { + name: try_merge_opt!(name: self, another), + description: try_merge_opt!(description: self, another), + deprecated: try_merge_opt!(deprecated: self, another), + ignore: try_merge_opt!(ignore: self, another), + downcast: try_merge_opt!(downcast: self, another), + }) + } + + /// Parses [`Attr`] from the given multiple `name`d [`syn::Attribute`]s + /// placed on a [GraphQL field][1] definition. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + pub(crate) fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result { + let mut attr = filter_attrs(name, attrs) + .map(|attr| attr.parse_args()) + .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?; + + if let Some(ignore) = &attr.ignore { + if attr.name.is_some() + || attr.description.is_some() + || attr.deprecated.is_some() + || attr.downcast.is_some() + { + return Err(syn::Error::new( + ignore.span(), + "`ignore` attribute argument is not composable with any other arguments", + )); + } + } + + if let Some(downcast) = &attr.downcast { + if attr.name.is_some() + || attr.description.is_some() + || attr.deprecated.is_some() + || attr.ignore.is_some() + { + return Err(syn::Error::new( + downcast.span(), + "`downcast` attribute argument is not composable with any other arguments", + )); + } + } + + if attr.description.is_none() { + attr.description = get_doc_comment(attrs).map(|sc| { + let span = sc.span_ident(); + sc.map(|desc| syn::LitStr::new(&desc, span)) + }); + } + + if attr.deprecated.is_none() { + attr.deprecated = get_deprecated(attrs).map(|sc| { + let span = sc.span_ident(); + sc.map(|depr| depr.reason.map(|rsn| syn::LitStr::new(&rsn, span))) + }); + } + + Ok(attr) + } +} + +/// Representation of a [GraphQL field][1] for code generation. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields +#[derive(Debug)] +pub(crate) struct Definition { + /// Rust type that this [GraphQL field][1] is represented by (method return + /// type or struct field type). + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + pub(crate) ty: syn::Type, + + /// Name of this [GraphQL field][1] in GraphQL schema. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + pub(crate) name: String, + + /// [Description][2] of this [GraphQL field][1] to put into GraphQL schema. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + /// [2]: https://spec.graphql.org/June2018/#sec-Descriptions + pub(crate) description: Option, + + /// [Deprecation][2] of this [GraphQL field][1] to put into GraphQL schema. + /// + /// If inner [`Option`] is [`None`], then deprecation has no message + /// attached. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + /// [2]: https://spec.graphql.org/June2018/#sec-Deprecation + pub(crate) deprecated: Option>, + + /// Ident of the Rust method (or struct field) representing this + /// [GraphQL field][1]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + pub(crate) ident: syn::Ident, + + /// Rust [`MethodArgument`]s required to call the method representing this + /// [GraphQL field][1]. + /// + /// If [`None`] then this [GraphQL field][1] is represented by a struct + /// field. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + pub(crate) arguments: Option>, + + /// Indicator whether the Rust method representing this [GraphQL field][1] + /// has a [`syn::Receiver`]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + pub(crate) has_receiver: bool, + + /// Indicator whether this [GraphQL field][1] should be resolved + /// asynchronously. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + pub(crate) is_async: bool, +} + +impl Definition { + /// Indicates whether this [GraphQL field][1] is represented by a method, + /// not a struct field. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + #[must_use] + pub(crate) fn is_method(&self) -> bool { + self.arguments.is_some() + } + + /// Returns generated code that panics about unknown [GraphQL field][1] + /// tried to be resolved in the [`GraphQLValue::resolve_field`] method. + /// + /// [`GraphQLValue::resolve_field`]: juniper::GraphQLValue::resolve_field + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + #[must_use] + pub(crate) fn method_resolve_field_panic_no_field_tokens(scalar: &scalar::Type) -> TokenStream { + quote! { + panic!( + "Field `{}` not found on type `{}`", + field, + >::name(info).unwrap(), + ) + } + } + + /// Returns generated code that panics about [GraphQL fields][1] tried to be + /// resolved asynchronously in the [`GraphQLValue::resolve_field`] method + /// (which is synchronous itself). + /// + /// [`GraphQLValue::resolve_field`]: juniper::GraphQLValue::resolve_field + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + #[must_use] + pub(crate) fn method_resolve_field_panic_async_field_tokens( + field_names: &[&str], + scalar: &scalar::Type, + ) -> TokenStream { + quote! { + #( #field_names )|* => panic!( + "Tried to resolve async field `{}` on type `{}` with a sync resolver", + field, + >::name(info).unwrap(), + ), + } + } + + /// Returns generated code for the [`marker::IsOutputType::mark`] method, + /// which performs static checks for this [GraphQL field][1]. + /// + /// [`marker::IsOutputType::mark`]: juniper::marker::IsOutputType::mark + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + #[must_use] + pub(crate) fn method_mark_tokens( + &self, + infer_result: bool, + scalar: &scalar::Type, + ) -> TokenStream { + let args_marks = self + .arguments + .iter() + .flat_map(|args| args.iter().filter_map(|a| a.method_mark_tokens(scalar))); + + let ty = &self.ty; + let mut ty = quote! { #ty }; + if infer_result { + ty = quote! { + <#ty as ::juniper::IntoFieldResult::<_, #scalar>>::Item + }; + } + let resolved_ty = quote! { + <#ty as ::juniper::IntoResolvable< + '_, #scalar, _, >::Context, + >>::Type + }; + + quote! { + #( #args_marks )* + <#resolved_ty as ::juniper::marker::IsOutputType<#scalar>>::mark(); + } + } + + /// Returns generated code for the [`GraphQLType::meta`] method, which + /// registers this [GraphQL field][1] in [`Registry`]. + /// + /// [`GraphQLType::meta`]: juniper::GraphQLType::meta + /// [`Registry`]: juniper::Registry + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + #[must_use] + pub(crate) fn method_meta_tokens( + &self, + extract_stream_type: Option<&scalar::Type>, + ) -> TokenStream { + let (name, ty) = (&self.name, &self.ty); + let mut ty = quote! { #ty }; + if let Some(scalar) = extract_stream_type { + ty = quote! { + <#ty as ::juniper::ExtractTypeFromStream<_, #scalar>>::Item + }; + } + + let description = self + .description + .as_ref() + .map(|desc| quote! { .description(#desc) }); + + let deprecated = self.deprecated.as_ref().map(|reason| { + let reason = reason + .as_ref() + .map(|rsn| quote! { Some(#rsn) }) + .unwrap_or_else(|| quote! { None }); + quote! { .deprecated(#reason) } + }); + + let args = self + .arguments + .iter() + .flat_map(|args| args.iter().filter_map(MethodArgument::method_meta_tokens)); + + quote! { + registry.field_convert::<#ty, _, Self::Context>(#name, info) + #( #args )* + #description + #deprecated + } + } + + /// Returns generated code for the [`GraphQLValue::resolve_field`][0] + /// method, which resolves this [GraphQL field][1] synchronously. + /// + /// Returns [`None`] if this [`Definition::is_async`]. + /// + /// [0]: juniper::GraphQLValue::resolve_field + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + #[must_use] + pub(crate) fn method_resolve_field_tokens( + &self, + scalar: &scalar::Type, + trait_ty: Option<&syn::Type>, + ) -> Option { + if self.is_async { + return None; + } + + let (name, mut ty, ident) = (&self.name, self.ty.clone(), &self.ident); + + let res = if self.is_method() { + let args = self + .arguments + .as_ref() + .unwrap() + .iter() + .map(|arg| arg.method_resolve_field_tokens(scalar)); + + let rcv = self.has_receiver.then(|| { + quote! { self, } + }); + + if trait_ty.is_some() { + quote! { ::#ident(#rcv #( #args ),*) } + } else { + quote! { Self::#ident(#rcv #( #args ),*) } + } + } else { + ty = parse_quote! { _ }; + quote! { &self.#ident } + }; + + let resolving_code = gen::sync_resolving_code(); + + Some(quote! { + #name => { + let res: #ty = #res; + #resolving_code + } + }) + } + + /// Returns generated code for the + /// [`GraphQLValueAsync::resolve_field_async`][0] method, which resolves + /// this [GraphQL field][1] asynchronously. + /// + /// [0]: juniper::GraphQLValueAsync::resolve_field_async + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + #[must_use] + pub(crate) fn method_resolve_field_async_tokens( + &self, + scalar: &scalar::Type, + trait_ty: Option<&syn::Type>, + ) -> TokenStream { + let (name, mut ty, ident) = (&self.name, self.ty.clone(), &self.ident); + + let mut fut = if self.is_method() { + let args = self + .arguments + .as_ref() + .unwrap() + .iter() + .map(|arg| arg.method_resolve_field_tokens(scalar)); + + let rcv = self.has_receiver.then(|| { + quote! { self, } + }); + + if trait_ty.is_some() { + quote! { ::#ident(#rcv #( #args ),*) } + } else { + quote! { Self::#ident(#rcv #( #args ),*) } + } + } else { + ty = parse_quote! { _ }; + quote! { &self.#ident } + }; + if !self.is_async { + fut = quote! { ::juniper::futures::future::ready(#fut) }; + } + + let resolving_code = gen::async_resolving_code(Some(&ty)); + + quote! { + #name => { + let fut = #fut; + #resolving_code + } + } + } + + /// Returns generated code for the + /// [`GraphQLSubscriptionValue::resolve_field_into_stream`][0] method, which + /// resolves this [GraphQL field][1] as [subscription][2]. + /// + /// [0]: juniper::GraphQLSubscriptionValue::resolve_field_into_stream + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields + /// [2]: https://spec.graphql.org/June2018/#sec-Subscription + #[must_use] + pub(crate) fn method_resolve_field_into_stream_tokens( + &self, + scalar: &scalar::Type, + ) -> TokenStream { + let (name, mut ty, ident) = (&self.name, self.ty.clone(), &self.ident); + + let mut fut = if self.is_method() { + let args = self + .arguments + .as_ref() + .unwrap() + .iter() + .map(|arg| arg.method_resolve_field_tokens(scalar)); + + let rcv = self.has_receiver.then(|| { + quote! { self, } + }); + + quote! { Self::#ident(#rcv #( #args ),*) } + } else { + ty = parse_quote! { _ }; + quote! { &self.#ident } + }; + if !self.is_async { + fut = quote! { ::juniper::futures::future::ready(#fut) }; + } + + quote! { + #name => { + ::juniper::futures::FutureExt::boxed(async move { + let res: #ty = #fut.await; + let res = ::juniper::IntoFieldResult::<_, #scalar>::into_result(res)?; + let executor = executor.as_owned_executor(); + let stream = ::juniper::futures::StreamExt::then(res, move |res| { + let executor = executor.clone(); + let res2: ::juniper::FieldResult<_, #scalar> = + ::juniper::IntoResolvable::into(res, executor.context()); + async move { + let ex = executor.as_executor(); + match res2 { + Ok(Some((ctx, r))) => { + let sub = ex.replaced_context(ctx); + sub.resolve_with_ctx_async(&(), &r) + .await + .map_err(|e| ex.new_error(e)) + } + Ok(None) => Ok(::juniper::Value::null()), + Err(e) => Err(ex.new_error(e)), + } + } + }); + Ok(::juniper::Value::Scalar::< + ::juniper::ValuesStream::<#scalar> + >(::juniper::futures::StreamExt::boxed(stream))) + }) + } + } + } +} + +/// Checks whether all [GraphQL fields][1] fields have different names. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields +#[must_use] +pub(crate) fn all_different(fields: &[Definition]) -> bool { + let mut names: Vec<_> = fields.iter().map(|f| &f.name).collect(); + names.dedup(); + names.len() == fields.len() +} diff --git a/juniper_codegen/src/common/mod.rs b/juniper_codegen/src/common/mod.rs index 88498c1e8..fd16d954d 100644 --- a/juniper_codegen/src/common/mod.rs +++ b/juniper_codegen/src/common/mod.rs @@ -1,89 +1,6 @@ //! Common functions, definitions and extensions for code generation, used by this crate. +pub(crate) mod field; pub(crate) mod gen; pub(crate) mod parse; - -use proc_macro2::TokenStream; -use quote::ToTokens; -use syn::parse_quote; - -/// [`ScalarValue`] parametrization of the code generation. -/// -/// [`ScalarValue`]: juniper::ScalarValue -#[derive(Clone, Debug)] -pub(crate) enum ScalarValueType { - /// Concrete Rust type is specified as [`ScalarValue`]. - /// - /// [`ScalarValue`]: juniper::ScalarValue - Concrete(syn::Type), - - /// One of type parameters of the original type is specified as [`ScalarValue`]. - /// - /// The original type is the type that the code is generated for. - /// - /// [`ScalarValue`]: juniper::ScalarValue - ExplicitGeneric(syn::Ident), - - /// [`ScalarValue`] parametrization is assumed to be a generic and is not specified explicitly. - /// - /// [`ScalarValue`]: juniper::ScalarValue - ImplicitGeneric, -} - -impl ScalarValueType { - /// Indicates whether this [`ScalarValueType`] is generic. - #[must_use] - pub(crate) fn is_generic(&self) -> bool { - matches!(self, Self::ExplicitGeneric(_) | Self::ImplicitGeneric) - } - - /// Indicates whether this [`ScalarValueType`] is [`ScalarValueType::ExplicitGeneric`]. - #[must_use] - pub(crate) fn is_explicit_generic(&self) -> bool { - matches!(self, Self::ExplicitGeneric(_)) - } - - /// Indicates whether this [`ScalarValueType`] is [`ScalarValueType::ImplicitGeneric`]. - #[must_use] - pub(crate) fn is_implicit_generic(&self) -> bool { - matches!(self, Self::ImplicitGeneric) - } - - /// Returns a type identifier which represents this [`ScalarValueType`]. - #[must_use] - pub(crate) fn ty(&self) -> syn::Type { - match self { - Self::Concrete(ty) => ty.clone(), - Self::ExplicitGeneric(ty_param) => parse_quote! { #ty_param }, - Self::ImplicitGeneric => parse_quote! { __S }, - } - } - - /// Returns a type parameter identifier that suits this [`ScalarValueType`]. - #[must_use] - pub(crate) fn generic_ty(&self) -> syn::Type { - match self { - Self::ExplicitGeneric(ty_param) => parse_quote! { #ty_param }, - Self::ImplicitGeneric | Self::Concrete(_) => parse_quote! { __S }, - } - } - - /// Returns a default [`ScalarValue`] type that is compatible with this [`ScalarValueType`]. - /// - /// [`ScalarValue`]: juniper::ScalarValue - #[must_use] - pub(crate) fn default_ty(&self) -> syn::Type { - match self { - Self::Concrete(ty) => ty.clone(), - Self::ExplicitGeneric(_) | Self::ImplicitGeneric => { - parse_quote! { ::juniper::DefaultScalarValue } - } - } - } -} - -impl ToTokens for ScalarValueType { - fn to_tokens(&self, into: &mut TokenStream) { - self.ty().to_tokens(into) - } -} +pub(crate) mod scalar; diff --git a/juniper_codegen/src/common/parse/mod.rs b/juniper_codegen/src/common/parse/mod.rs index c3ec54f7c..cae19a015 100644 --- a/juniper_codegen/src/common/parse/mod.rs +++ b/juniper_codegen/src/common/parse/mod.rs @@ -95,19 +95,30 @@ impl<'a> ParseBufferExt for ParseBuffer<'a> { /// Extension of [`syn::Type`] providing common function widely used by this crate for parsing. pub(crate) trait TypeExt { - /// Retrieves the innermost non-parenthesized [`syn::Type`] from the given one (unwraps nested - /// [`syn::TypeParen`]s asap). + /// Retrieves the innermost non-parenthesized [`syn::Type`] from the given + /// one (unwraps nested [`syn::TypeParen`]s asap). #[must_use] fn unparenthesized(&self) -> &Self; - /// Retrieves the inner [`syn::Type`] from the given reference type, or just returns "as is" if - /// the type is not a reference. + /// Retrieves the inner [`syn::Type`] from the given reference type, or just + /// returns "as is" if the type is not a reference. /// /// Also, makes the type [`TypeExt::unparenthesized`], if possible. #[must_use] fn unreferenced(&self) -> &Self; + /// Iterates mutably over all the lifetime parameters of this [`syn::Type`] + /// with the given `func`tion. + fn lifetimes_iter_mut(&mut self, func: &mut F); + + /// Anonymizes all the lifetime parameters of this [`syn::Type`] (except + /// the `'static` ones), making it suitable for using in contexts with + /// inferring. fn lifetimes_anonymized(&mut self); + + /// Returns the topmost [`syn::Ident`] of this [`syn::TypePath`], if any. + #[must_use] + fn topmost_ident(&self) -> Option<&syn::Ident>; } impl TypeExt for syn::Type { @@ -125,19 +136,45 @@ impl TypeExt for syn::Type { } } - fn lifetimes_anonymized(&mut self) { + fn lifetimes_iter_mut(&mut self, func: &mut F) { use syn::{GenericArgument as GA, Type as T}; + fn iter_path(path: &mut syn::Path, func: &mut F) { + for seg in path.segments.iter_mut() { + match &mut seg.arguments { + syn::PathArguments::AngleBracketed(angle) => { + for arg in angle.args.iter_mut() { + match arg { + GA::Lifetime(lt) => func(lt), + GA::Type(ty) => ty.lifetimes_iter_mut(func), + GA::Binding(b) => b.ty.lifetimes_iter_mut(func), + GA::Constraint(_) | GA::Const(_) => {} + } + } + } + syn::PathArguments::Parenthesized(args) => { + for ty in args.inputs.iter_mut() { + ty.lifetimes_iter_mut(func) + } + if let syn::ReturnType::Type(_, ty) = &mut args.output { + (&mut *ty).lifetimes_iter_mut(func) + } + } + syn::PathArguments::None => {} + } + } + } + match self { T::Array(syn::TypeArray { elem, .. }) | T::Group(syn::TypeGroup { elem, .. }) | T::Paren(syn::TypeParen { elem, .. }) | T::Ptr(syn::TypePtr { elem, .. }) - | T::Slice(syn::TypeSlice { elem, .. }) => (&mut *elem).lifetimes_anonymized(), + | T::Slice(syn::TypeSlice { elem, .. }) => (&mut *elem).lifetimes_iter_mut(func), T::Tuple(syn::TypeTuple { elems, .. }) => { for ty in elems.iter_mut() { - ty.lifetimes_anonymized(); + ty.lifetimes_iter_mut(func) } } @@ -145,11 +182,12 @@ impl TypeExt for syn::Type { | T::TraitObject(syn::TypeTraitObject { bounds, .. }) => { for bound in bounds.iter_mut() { match bound { - syn::TypeParamBound::Lifetime(lt) => { - lt.ident = syn::Ident::new("_", Span::call_site()) - } - syn::TypeParamBound::Trait(_) => { - todo!("Anonymizing lifetimes in trait is not yet supported") + syn::TypeParamBound::Lifetime(lt) => func(lt), + syn::TypeParamBound::Trait(bound) => { + if bound.lifetimes.is_some() { + todo!("Iterating over HRTB lifetimes in trait is not yet supported") + } + iter_path(&mut bound.path, func) } } } @@ -157,43 +195,17 @@ impl TypeExt for syn::Type { T::Reference(ref_ty) => { if let Some(lt) = ref_ty.lifetime.as_mut() { - lt.ident = syn::Ident::new("_", Span::call_site()); + func(lt) } - (&mut *ref_ty.elem).lifetimes_anonymized(); + (&mut *ref_ty.elem).lifetimes_iter_mut(func) } - T::Path(ty) => { - for seg in ty.path.segments.iter_mut() { - match &mut seg.arguments { - syn::PathArguments::AngleBracketed(angle) => { - for arg in angle.args.iter_mut() { - match arg { - GA::Lifetime(lt) => { - lt.ident = syn::Ident::new("_", Span::call_site()); - } - GA::Type(ty) => ty.lifetimes_anonymized(), - GA::Binding(b) => b.ty.lifetimes_anonymized(), - GA::Constraint(_) | GA::Const(_) => {} - } - } - } - syn::PathArguments::Parenthesized(args) => { - for ty in args.inputs.iter_mut() { - ty.lifetimes_anonymized(); - } - if let syn::ReturnType::Type(_, ty) = &mut args.output { - (&mut *ty).lifetimes_anonymized(); - } - } - syn::PathArguments::None => {} - } - } - } + T::Path(ty) => iter_path(&mut ty.path, func), // These types unlikely will be used as GraphQL types. T::BareFn(_) | T::Infer(_) | T::Macro(_) | T::Never(_) | T::Verbatim(_) => {} - // Following the syn idiom for exhaustive matching on Type + // Following the syn idiom for exhaustive matching on Type: // https://github.com/dtolnay/syn/blob/master/src/ty.rs#L66-L88 #[cfg(test)] T::__TestExhaustive(_) => unimplemented!(), @@ -202,6 +214,32 @@ impl TypeExt for syn::Type { _ => {} } } + + fn lifetimes_anonymized(&mut self) { + self.lifetimes_iter_mut(&mut |lt| { + if lt.ident != "_" && lt.ident != "static" { + lt.ident = syn::Ident::new("_", Span::call_site()); + } + }); + } + + fn topmost_ident(&self) -> Option<&syn::Ident> { + match self.unparenthesized() { + syn::Type::Path(p) => Some(&p.path), + syn::Type::Reference(r) => match (&*r.elem).unparenthesized() { + syn::Type::Path(p) => Some(&p.path), + syn::Type::TraitObject(o) => match o.bounds.iter().next().unwrap() { + syn::TypeParamBound::Trait(b) => Some(&b.path), + _ => None, + }, + _ => None, + }, + _ => None, + }? + .segments + .last() + .map(|s| &s.ident) + } } /// Extension of [`syn::Generics`] providing common function widely used by this crate for parsing. diff --git a/juniper_codegen/src/common/scalar.rs b/juniper_codegen/src/common/scalar.rs new file mode 100644 index 000000000..1569a5391 --- /dev/null +++ b/juniper_codegen/src/common/scalar.rs @@ -0,0 +1,173 @@ +//! Common functions, definitions and extensions for parsing and code generation +//! related to [`ScalarValue`]. +//! +//! [`ScalarValue`]: juniper::ScalarValue + +use proc_macro2::{Span, TokenStream}; +use quote::ToTokens; +use syn::{ + parse::{Parse, ParseStream}, + parse_quote, + spanned::Spanned, +}; + +/// Possible values of `#[graphql(scalar = ...)]` attribute. +#[derive(Clone, Debug)] +pub(crate) enum AttrValue { + /// Concrete Rust type (like `DefaultScalarValue`). + /// + /// [`ScalarValue`]: juniper::ScalarValue + Concrete(syn::Type), + + /// Generic Rust type parameter with a bound predicate + /// (like `S: ScalarValue + Send + Sync`). + /// + /// [`ScalarValue`]: juniper::ScalarValue + Generic(syn::PredicateType), +} + +impl Parse for AttrValue { + fn parse(input: ParseStream<'_>) -> syn::Result { + if input.fork().parse::().is_ok() { + let pred = input.parse().unwrap(); + if let syn::WherePredicate::Type(p) = pred { + Ok(Self::Generic(p)) + } else { + Err(syn::Error::new( + pred.span(), + "only type predicates are allowed here", + )) + } + } else { + input.parse::().map(Self::Concrete) + } + } +} + +impl Spanned for AttrValue { + fn span(&self) -> Span { + match self { + Self::Concrete(ty) => ty.span(), + Self::Generic(pred) => pred.span(), + } + } +} + +/// [`ScalarValue`] parametrization of the code generation. +/// +/// [`ScalarValue`]: juniper::ScalarValue +#[derive(Clone, Debug)] +pub(crate) enum Type { + /// Concrete Rust type is specified as [`ScalarValue`]. + /// + /// [`ScalarValue`]: juniper::ScalarValue + Concrete(syn::Type), + + /// One of type parameters of the original type is specified as [`ScalarValue`]. + /// + /// The original type is the type that the code is generated for. + /// + /// [`ScalarValue`]: juniper::ScalarValue + ExplicitGeneric(syn::Ident), + + /// [`ScalarValue`] parametrization is assumed to be generic and is not specified + /// explicitly, or specified as bound predicate (like `S: ScalarValue + Send + Sync`). + /// + /// [`ScalarValue`]: juniper::ScalarValue + ImplicitGeneric(Option), +} + +impl ToTokens for Type { + fn to_tokens(&self, into: &mut TokenStream) { + self.ty().to_tokens(into) + } +} + +impl Type { + /// Indicates whether this [`Type`] is generic. + #[must_use] + pub(crate) fn is_generic(&self) -> bool { + matches!(self, Self::ExplicitGeneric(_) | Self::ImplicitGeneric(_)) + } + + /// Indicates whether this [`Type`] is [`Type::ExplicitGeneric`]. + #[must_use] + pub(crate) fn is_explicit_generic(&self) -> bool { + matches!(self, Self::ExplicitGeneric(_)) + } + + /// Indicates whether this [`Type`] is [`Type::ImplicitGeneric`]. + #[must_use] + pub(crate) fn is_implicit_generic(&self) -> bool { + matches!(self, Self::ImplicitGeneric(_)) + } + + /// Returns additional trait bounds behind this [`Type`], if any. + #[must_use] + pub(crate) fn bounds(&self) -> Option { + if let Self::ImplicitGeneric(Some(pred)) = self { + Some(syn::WherePredicate::Type(pred.clone())) + } else { + None + } + } + + /// Returns a type identifier which represents this [`Type`]. + #[must_use] + pub(crate) fn ty(&self) -> syn::Type { + match self { + Self::Concrete(ty) => ty.clone(), + Self::ExplicitGeneric(ty_param) => parse_quote! { #ty_param }, + Self::ImplicitGeneric(Some(pred)) => pred.bounded_ty.clone(), + Self::ImplicitGeneric(None) => parse_quote! { __S }, + } + } + + /// Returns a type parameter identifier that suits this [`Type`]. + #[must_use] + pub(crate) fn generic_ty(&self) -> syn::Type { + match self { + Self::ExplicitGeneric(ty_param) => parse_quote! { #ty_param }, + Self::ImplicitGeneric(Some(pred)) => pred.bounded_ty.clone(), + Self::ImplicitGeneric(None) | Self::Concrete(_) => parse_quote! { __S }, + } + } + + /// Returns a default [`ScalarValue`] type that is compatible with this [`Type`]. + /// + /// [`ScalarValue`]: juniper::ScalarValue + #[must_use] + pub(crate) fn default_ty(&self) -> syn::Type { + match self { + Self::Concrete(ty) => ty.clone(), + Self::ExplicitGeneric(_) | Self::ImplicitGeneric(_) => { + parse_quote! { ::juniper::DefaultScalarValue } + } + } + } + + /// Parses [`Type`] from the given `explicit` [`AttrValue`] (if any), + /// checking whether it's contained in the giving `generics`. + #[must_use] + pub(crate) fn parse(explicit: Option<&AttrValue>, generics: &syn::Generics) -> Self { + match explicit { + Some(AttrValue::Concrete(scalar_ty)) => generics + .params + .iter() + .find_map(|p| { + if let syn::GenericParam::Type(tp) = p { + let ident = &tp.ident; + let ty: syn::Type = parse_quote! { #ident }; + if &ty == scalar_ty { + return Some(&tp.ident); + } + } + None + }) + .map(|ident| Self::ExplicitGeneric(ident.clone())) + .unwrap_or_else(|| Self::Concrete(scalar_ty.clone())), + Some(AttrValue::Generic(pred)) => Self::ImplicitGeneric(Some(pred.clone())), + None => Self::ImplicitGeneric(None), + } + } +} diff --git a/juniper_codegen/src/derive_object.rs b/juniper_codegen/src/derive_object.rs deleted file mode 100644 index 6d0af8fa4..000000000 --- a/juniper_codegen/src/derive_object.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::{ - result::{GraphQLScope, UnsupportedAttribute}, - util::{self, span_container::SpanContainer, RenameRule}, -}; -use proc_macro2::TokenStream; -use quote::quote; -use syn::{self, ext::IdentExt, spanned::Spanned, Data, Fields}; - -pub fn build_derive_object(ast: syn::DeriveInput, error: GraphQLScope) -> syn::Result { - let ast_span = ast.span(); - let struct_fields = match ast.data { - Data::Struct(data) => match data.fields { - Fields::Named(fields) => fields.named, - _ => return Err(error.custom_error(ast_span, "only named fields are allowed")), - }, - _ => return Err(error.custom_error(ast_span, "can only be applied to structs")), - }; - - // Parse attributes. - let attrs = util::ObjectAttributes::from_attrs(&ast.attrs)?; - - let ident = &ast.ident; - let name = attrs - .name - .clone() - .map(SpanContainer::into_inner) - .unwrap_or_else(|| ident.unraw().to_string()); - - let fields = struct_fields - .into_iter() - .filter_map(|field| { - let span = field.span(); - let field_attrs = match util::FieldAttributes::from_attrs( - &field.attrs, - util::FieldAttributeParseMode::Object, - ) { - Ok(attrs) => attrs, - Err(e) => { - proc_macro_error::emit_error!(e); - return None; - } - }; - - if field_attrs.skip.is_some() { - return None; - } - - let field_name = &field.ident.unwrap(); - let name = field_attrs - .name - .clone() - .map(SpanContainer::into_inner) - .unwrap_or_else(|| { - attrs - .rename - .unwrap_or(RenameRule::CamelCase) - .apply(&field_name.unraw().to_string()) - }); - - if name.starts_with("__") { - error.no_double_underscore(if let Some(name) = field_attrs.name { - name.span_ident() - } else { - field_name.span() - }); - } - - if let Some(default) = field_attrs.default { - error.unsupported_attribute_within( - default.span_ident(), - UnsupportedAttribute::Default, - ); - } - - let resolver_code = quote!( - &self . #field_name - ); - - Some(util::GraphQLTypeDefinitionField { - name, - _type: field.ty, - args: Vec::new(), - description: field_attrs.description.map(SpanContainer::into_inner), - deprecation: field_attrs.deprecation.map(SpanContainer::into_inner), - resolver_code, - default: None, - is_type_inferred: true, - is_async: false, - span, - }) - }) - .collect::>(); - - // Early abort after checking all fields - proc_macro_error::abort_if_dirty(); - - if let Some(duplicates) = - crate::util::duplicate::Duplicate::find_by_key(&fields, |field| field.name.as_str()) - { - error.duplicate(duplicates.iter()); - } - - if !attrs.is_internal && name.starts_with("__") { - error.no_double_underscore(if let Some(name) = attrs.name { - name.span_ident() - } else { - ident.span() - }); - } - - if fields.is_empty() { - error.not_empty(ast_span); - } - - // Early abort after GraphQL properties - proc_macro_error::abort_if_dirty(); - - let definition = util::GraphQLTypeDefiniton { - name, - _type: syn::parse_str(&ast.ident.to_string()).unwrap(), - context: attrs.context.map(SpanContainer::into_inner), - scalar: attrs.scalar.map(SpanContainer::into_inner), - description: attrs.description.map(SpanContainer::into_inner), - fields, - generics: ast.generics, - interfaces: attrs - .interfaces - .into_iter() - .map(SpanContainer::into_inner) - .collect(), - include_type_generics: true, - generic_scalar: true, - no_async: attrs.no_async.is_some(), - }; - - Ok(definition.into_tokens()) -} diff --git a/juniper_codegen/src/graphql_interface/attr.rs b/juniper_codegen/src/graphql_interface/attr.rs index 44187befa..2aedbb2ad 100644 --- a/juniper_codegen/src/graphql_interface/attr.rs +++ b/juniper_codegen/src/graphql_interface/attr.rs @@ -8,16 +8,17 @@ use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned}; use crate::{ common::{ + field, parse::{self, TypeExt as _}, - ScalarValueType, + scalar, }, result::GraphQLScope, - util::{path_eq_single, span_container::SpanContainer, to_camel_case}, + util::{path_eq_single, span_container::SpanContainer, RenameRule}, }; use super::{ - inject_async_trait, ArgumentMeta, Definition, EnumType, Field, FieldArgument, ImplMeta, - Implementer, ImplementerDowncast, MethodArgument, MethodMeta, TraitMeta, TraitObjectType, Type, + inject_async_trait, Definition, EnumType, ImplAttr, Implementer, ImplementerDowncast, + TraitAttr, TraitObjectType, Type, }; /// [`GraphQLScope`] of errors for `#[graphql_interface]` macro. @@ -45,62 +46,42 @@ pub fn expand(attr_args: TokenStream, body: TokenStream) -> syn::Result, mut ast: syn::ItemTrait, ) -> syn::Result { - let meta = TraitMeta::from_attrs("graphql_interface", &attrs)?; + let attr = TraitAttr::from_attrs("graphql_interface", &attrs)?; let trait_ident = &ast.ident; let trait_span = ast.span(); - let name = meta + let name = attr .name .clone() .map(SpanContainer::into_inner) .unwrap_or_else(|| trait_ident.unraw().to_string()); - if !meta.is_internal && name.starts_with("__") { + if !attr.is_internal && name.starts_with("__") { ERR.no_double_underscore( - meta.name + attr.name .as_ref() .map(SpanContainer::span_ident) .unwrap_or_else(|| trait_ident.span()), ); } - let scalar = meta - .scalar - .as_ref() - .map(|sc| { - ast.generics - .params - .iter() - .find_map(|p| { - if let syn::GenericParam::Type(tp) = p { - let ident = &tp.ident; - let ty: syn::Type = parse_quote! { #ident }; - if &ty == sc.as_ref() { - return Some(&tp.ident); - } - } - None - }) - .map(|ident| ScalarValueType::ExplicitGeneric(ident.clone())) - .unwrap_or_else(|| ScalarValueType::Concrete(sc.as_ref().clone())) - }) - .unwrap_or_else(|| ScalarValueType::ImplicitGeneric); + let scalar = scalar::Type::parse(attr.scalar.as_deref(), &ast.generics); - let mut implementers: Vec<_> = meta + let mut implementers: Vec<_> = attr .implementers .iter() .map(|ty| Implementer { ty: ty.as_ref().clone(), downcast: None, - context_ty: None, + context: None, scalar: scalar.clone(), }) .collect(); - for (ty, downcast) in &meta.external_downcasts { + for (ty, downcast) in &attr.external_downcasts { match implementers.iter_mut().find(|i| &i.ty == ty) { Some(impler) => { impler.downcast = Some(ImplementerDowncast::External { @@ -113,10 +94,16 @@ pub fn expand_on_trait( proc_macro_error::abort_if_dirty(); + let renaming = attr + .rename_fields + .as_deref() + .copied() + .unwrap_or(RenameRule::CamelCase); + let mut fields = vec![]; for item in &mut ast.items { if let syn::TraitItem::Method(m) = item { - match TraitMethod::parse(m) { + match TraitMethod::parse(m, &renaming) { Some(TraitMethod::Field(f)) => fields.push(f), Some(TraitMethod::Downcast(d)) => { match implementers.iter_mut().find(|i| i.ty == d.ty) { @@ -125,7 +112,7 @@ pub fn expand_on_trait( err_duplicate_downcast(m, external, &impler.ty); } else { impler.downcast = d.downcast; - impler.context_ty = d.context_ty; + impler.context = d.context; } } None => err_only_implementer_downcast(&m.sig), @@ -141,35 +128,36 @@ pub fn expand_on_trait( if fields.is_empty() { ERR.emit_custom(trait_span, "must have at least one field"); } - - if !all_fields_different(&fields) { + if !field::all_different(&fields) { ERR.emit_custom(trait_span, "must have a different name for each field"); } proc_macro_error::abort_if_dirty(); - let context = meta + let context = attr .context - .as_ref() - .map(|c| c.as_ref().clone()) + .as_deref() + .cloned() .or_else(|| { fields.iter().find_map(|f| { - f.arguments - .iter() - .find_map(MethodArgument::context_ty) - .cloned() + f.arguments.as_ref().and_then(|f| { + f.iter() + .find_map(field::MethodArgument::context_ty) + .cloned() + }) }) }) .or_else(|| { implementers .iter() - .find_map(|impler| impler.context_ty.as_ref()) + .find_map(|impler| impler.context.as_ref()) .cloned() - }); + }) + .unwrap_or_else(|| parse_quote! { () }); - let is_trait_object = meta.r#dyn.is_some(); + let is_trait_object = attr.r#dyn.is_some(); - let is_async_trait = meta.asyncness.is_some() + let is_async_trait = attr.asyncness.is_some() || ast .items .iter() @@ -186,14 +174,14 @@ pub fn expand_on_trait( let ty = if is_trait_object { Type::TraitObject(Box::new(TraitObjectType::new( &ast, - &meta, + &attr, scalar.clone(), context.clone(), ))) } else { Type::Enum(Box::new(EnumType::new( &ast, - &meta, + &attr, &implementers, scalar.clone(), ))) @@ -203,7 +191,7 @@ pub fn expand_on_trait( ty, name, - description: meta.description.map(SpanContainer::into_inner), + description: attr.description.map(SpanContainer::into_inner), context, scalar: scalar.clone(), @@ -253,19 +241,15 @@ pub fn expand_on_trait( Ok(quote! { #ast - #generated_code }) } -/// Expands `#[graphql_interface]` macro placed on trait implementation block. -pub fn expand_on_impl( - attrs: Vec, - mut ast: syn::ItemImpl, -) -> syn::Result { - let meta = ImplMeta::from_attrs("graphql_interface", &attrs)?; +/// Expands `#[graphql_interface]` macro placed on a trait implementation block. +fn expand_on_impl(attrs: Vec, mut ast: syn::ItemImpl) -> syn::Result { + let attr = ImplAttr::from_attrs("graphql_interface", &attrs)?; - let is_async_trait = meta.asyncness.is_some() + let is_async_trait = attr.asyncness.is_some() || ast .items .iter() @@ -275,30 +259,10 @@ pub fn expand_on_impl( }) .is_some(); - let is_trait_object = meta.r#dyn.is_some(); + let is_trait_object = attr.r#dyn.is_some(); if is_trait_object { - let scalar = meta - .scalar - .as_ref() - .map(|sc| { - ast.generics - .params - .iter() - .find_map(|p| { - if let syn::GenericParam::Type(tp) = p { - let ident = &tp.ident; - let ty: syn::Type = parse_quote! { #ident }; - if &ty == sc.as_ref() { - return Some(&tp.ident); - } - } - None - }) - .map(|ident| ScalarValueType::ExplicitGeneric(ident.clone())) - .unwrap_or_else(|| ScalarValueType::Concrete(sc.as_ref().clone())) - }) - .unwrap_or_else(|| ScalarValueType::ImplicitGeneric); + let scalar = scalar::Type::parse(attr.scalar.as_deref(), &ast.generics); ast.attrs.push(parse_quote! { #[allow(unused_qualifications, clippy::type_repetition_in_bounds)] @@ -348,7 +312,7 @@ enum TraitMethod { /// Method represents a [`Field`] of [GraphQL interface][1]. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces - Field(Field), + Field(field::Definition), /// Method represents a custom downcasting function into the [`Implementer`] of /// [GraphQL interface][1]. @@ -363,7 +327,7 @@ impl TraitMethod { /// Returns [`None`] if the trait method marked with `#[graphql(ignore)]` attribute, /// or parsing fails. #[must_use] - fn parse(method: &mut syn::TraitItemMethod) -> Option { + fn parse(method: &mut syn::TraitItemMethod, renaming: &RenameRule) -> Option { let method_attrs = method.attrs.clone(); // Remove repeated attributes from the method, to omit incorrect expansion. @@ -372,19 +336,19 @@ impl TraitMethod { .filter(|attr| !path_eq_single(&attr.path, "graphql")) .collect(); - let meta = MethodMeta::from_attrs("graphql", &method_attrs) + let attr = field::Attr::from_attrs("graphql", &method_attrs) .map_err(|e| proc_macro_error::emit_error!(e)) .ok()?; - if meta.ignore.is_some() { + if attr.ignore.is_some() { return None; } - if meta.downcast.is_some() { + if attr.downcast.is_some() { return Some(Self::Downcast(Box::new(Self::parse_downcast(method)?))); } - Some(Self::Field(Self::parse_field(method, meta)?)) + Some(Self::Field(Self::parse_field(method, attr, renaming)?)) } /// Parses [`TraitMethod::Downcast`] from the given trait method definition. @@ -426,8 +390,8 @@ impl TraitMethod { Some(Implementer { ty, downcast: Some(downcast), - context_ty, - scalar: ScalarValueType::ImplicitGeneric, + context: context_ty, + scalar: scalar::Type::ImplicitGeneric(None), }) } @@ -435,17 +399,21 @@ impl TraitMethod { /// /// Returns [`None`] if parsing fails. #[must_use] - fn parse_field(method: &mut syn::TraitItemMethod, meta: MethodMeta) -> Option { + fn parse_field( + method: &mut syn::TraitItemMethod, + attr: field::Attr, + renaming: &RenameRule, + ) -> Option { let method_ident = &method.sig.ident; - let name = meta + let name = attr .name .as_ref() .map(|m| m.as_ref().value()) - .unwrap_or_else(|| to_camel_case(&method_ident.unraw().to_string())); + .unwrap_or_else(|| renaming.apply(&method_ident.unraw().to_string())); if name.starts_with("__") { ERR.no_double_underscore( - meta.name + attr.name .as_ref() .map(SpanContainer::span_ident) .unwrap_or_else(|| method_ident.span()), @@ -476,7 +444,7 @@ impl TraitMethod { args_iter .filter_map(|arg| match arg { syn::FnArg::Receiver(_) => None, - syn::FnArg::Typed(arg) => Self::parse_field_argument(arg), + syn::FnArg::Typed(arg) => field::MethodArgument::parse(arg, renaming, &ERR), }) .collect() }; @@ -487,161 +455,58 @@ impl TraitMethod { }; ty.lifetimes_anonymized(); - let description = meta.description.as_ref().map(|d| d.as_ref().value()); - let deprecated = meta + let description = attr.description.as_ref().map(|d| d.as_ref().value()); + let deprecated = attr .deprecated - .as_ref() - .map(|d| d.as_ref().as_ref().map(syn::LitStr::value)); + .as_deref() + .map(|d| d.as_ref().map(syn::LitStr::value)); - Some(Field { + Some(field::Definition { name, ty, description, deprecated, - method: method_ident.clone(), - arguments, + ident: method_ident.clone(), + arguments: Some(arguments), + has_receiver: method.sig.receiver().is_some(), is_async: method.sig.asyncness.is_some(), }) } - - /// Parses [`MethodArgument`] from the given trait method argument definition. - /// - /// Returns [`None`] if parsing fails. - #[must_use] - fn parse_field_argument(argument: &mut syn::PatType) -> Option { - let argument_attrs = argument.attrs.clone(); - - // Remove repeated attributes from the method, to omit incorrect expansion. - argument.attrs = mem::take(&mut argument.attrs) - .into_iter() - .filter(|attr| !path_eq_single(&attr.path, "graphql")) - .collect(); - - let meta = ArgumentMeta::from_attrs("graphql", &argument_attrs) - .map_err(|e| proc_macro_error::emit_error!(e)) - .ok()?; - - if meta.context.is_some() { - return Some(MethodArgument::Context(argument.ty.unreferenced().clone())); - } - if meta.executor.is_some() { - return Some(MethodArgument::Executor); - } - if let syn::Pat::Ident(name) = &*argument.pat { - let arg = match name.ident.unraw().to_string().as_str() { - "context" | "ctx" => { - Some(MethodArgument::Context(argument.ty.unreferenced().clone())) - } - "executor" => Some(MethodArgument::Executor), - _ => None, - }; - if arg.is_some() { - ensure_no_regular_field_argument_meta(&meta)?; - return arg; - } - } - - let name = if let Some(name) = meta.name.as_ref() { - name.as_ref().value() - } else if let syn::Pat::Ident(name) = &*argument.pat { - to_camel_case(&name.ident.unraw().to_string()) - } else { - ERR.custom( - argument.pat.span(), - "trait method argument should be declared as a single identifier", - ) - .note(String::from( - "use `#[graphql(name = ...)]` attribute to specify custom argument's name without \ - requiring it being a single identifier", - )) - .emit(); - return None; - }; - if name.starts_with("__") { - ERR.no_double_underscore( - meta.name - .as_ref() - .map(SpanContainer::span_ident) - .unwrap_or_else(|| argument.pat.span()), - ); - return None; - } - - Some(MethodArgument::Regular(FieldArgument { - name, - ty: argument.ty.as_ref().clone(), - description: meta.description.as_ref().map(|d| d.as_ref().value()), - default: meta.default.as_ref().map(|v| v.as_ref().clone()), - })) - } -} - -/// Checks whether the given [`ArgumentMeta`] doesn't contain arguments related to -/// [`FieldArgument`]. -#[must_use] -fn ensure_no_regular_field_argument_meta(meta: &ArgumentMeta) -> Option<()> { - if let Some(span) = &meta.name { - return err_disallowed_attr(&span, "name"); - } - if let Some(span) = &meta.description { - return err_disallowed_attr(&span, "description"); - } - if let Some(span) = &meta.default { - return err_disallowed_attr(&span, "default"); - } - Some(()) } -/// Emits "argument is not allowed" [`syn::Error`] for the given `arg` pointing to the given `span`. -#[must_use] -fn err_disallowed_attr(span: &S, arg: &str) -> Option { - ERR.custom( - span.span(), - format!( - "attribute argument `#[graphql({} = ...)]` is not allowed here", - arg, - ), - ) - .emit(); - - None -} - -/// Emits "invalid trait method receiver" [`syn::Error`] pointing to the given `span`. +/// Emits "invalid trait method receiver" [`syn::Error`] pointing to the given +/// `span`. #[must_use] fn err_invalid_method_receiver(span: &S) -> Option { - ERR.custom( + ERR.emit_custom( span.span(), "trait method receiver can only be a shared reference `&self`", - ) - .emit(); - + ); None } -/// Emits "no trait method receiver" [`syn::Error`] pointing to the given `span`. +/// Emits "no trait method receiver" [`syn::Error`] pointing to the given +/// `span`. #[must_use] fn err_no_method_receiver(span: &S) -> Option { - ERR.custom( + ERR.emit_custom( span.span(), "trait method should have a shared reference receiver `&self`", - ) - .emit(); - + ); None } -/// Emits "non-implementer downcast target" [`syn::Error`] pointing to the given `span`. +/// Emits "non-implementer downcast target" [`syn::Error`] pointing to the given +/// `span`. fn err_only_implementer_downcast(span: &S) { - ERR.custom( + ERR.emit_custom( span.span(), "downcasting is possible only to interface implementers", - ) - .emit(); + ); } -/// Emits "duplicate downcast" [`syn::Error`] for the given `method` and `external` -/// [`ImplementerDowncast`] function. +/// Emits "duplicate downcast" [`syn::Error`] for the given `method` and +/// `external` [`ImplementerDowncast`] function. fn err_duplicate_downcast( method: &syn::TraitItemMethod, external: &ImplementerDowncast, @@ -655,25 +520,17 @@ fn err_duplicate_downcast( ERR.custom( method.span(), format!( - "trait method `{}` conflicts with the external downcast function `{}` declared on the \ - trait to downcast into the implementer type `{}`", + "trait method `{}` conflicts with the external downcast function \ + `{}` declared on the trait to downcast into the implementer type \ + `{}`", method.sig.ident, external.to_token_stream(), impler_ty.to_token_stream(), ), ) .note(String::from( - "use `#[graphql(ignore)]` attribute argument to ignore this trait method for interface \ - implementers downcasting", + "use `#[graphql(ignore)]` attribute argument to ignore this trait \ + method for interface implementers downcasting", )) .emit() } - -/// Checks whether all [GraphQL interface][1] fields have different names. -/// -/// [1]: https://spec.graphql.org/June2018/#sec-Interfaces -fn all_fields_different(fields: &[Field]) -> bool { - let mut names: Vec<_> = fields.iter().map(|f| &f.name).collect(); - names.dedup(); - names.len() == fields.len() -} diff --git a/juniper_codegen/src/graphql_interface/mod.rs b/juniper_codegen/src/graphql_interface/mod.rs index 78dfa81af..32d01cfeb 100644 --- a/juniper_codegen/src/graphql_interface/mod.rs +++ b/juniper_codegen/src/graphql_interface/mod.rs @@ -4,11 +4,15 @@ pub mod attr; -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + convert::TryInto as _, +}; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens, TokenStreamExt as _}; use syn::{ + ext::IdentExt as _, parse::{Parse, ParseStream}, parse_quote, spanned::Spanned as _, @@ -17,122 +21,133 @@ use syn::{ use crate::{ common::{ - gen, + field, gen, parse::{ attr::{err, OptionExt as _}, GenericsExt as _, ParseBufferExt as _, }, - ScalarValueType, + scalar, }, - util::{filter_attrs, get_deprecated, get_doc_comment, span_container::SpanContainer}, + util::{filter_attrs, get_doc_comment, span_container::SpanContainer, RenameRule}, }; -/// Available metadata (arguments) behind `#[graphql_interface]` attribute placed on a trait -/// definition, when generating code for [GraphQL interface][1] type. +/// Available arguments behind `#[graphql_interface]` attribute placed on a +/// trait definition, when generating code for [GraphQL interface][1] type. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces #[derive(Debug, Default)] -struct TraitMeta { +struct TraitAttr { /// Explicitly specified name of [GraphQL interface][1] type. /// - /// If absent, then Rust trait name is used by default. + /// If [`None`], then Rust trait name is used by default. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces name: Option>, /// Explicitly specified [description][2] of [GraphQL interface][1] type. /// - /// If absent, then Rust doc comment is used as [description][2], if any. + /// If [`None`], then Rust doc comment is used as [description][2], if any. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces /// [2]: https://spec.graphql.org/June2018/#sec-Descriptions description: Option>, - /// Explicitly specified identifier of the enum Rust type behind the trait, being an actual - /// implementation of a [GraphQL interface][1] type. + /// Explicitly specified identifier of the enum Rust type behind the trait, + /// being an actual implementation of a [GraphQL interface][1] type. /// - /// If absent, then `{trait_name}Value` identifier will be used. + /// If [`None`], then `{trait_name}Value` identifier will be used. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces r#enum: Option>, - /// Explicitly specified identifier of the Rust type alias of the [trait object][2], being an - /// actual implementation of a [GraphQL interface][1] type. + /// Explicitly specified identifier of the Rust type alias of the + /// [trait object][2], being an actual implementation of a + /// [GraphQL interface][1] type. /// - /// Effectively makes code generation to use a [trait object][2] as a [GraphQL interface][1] - /// type rather than an enum. If absent, then enum is used by default. + /// Effectively makes code generation to use a [trait object][2] as a + /// [GraphQL interface][1] type rather than an enum. If [`None`], then enum + /// is used by default. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces /// [2]: https://doc.rust-lang.org/reference/types/trait-object.html r#dyn: Option>, - /// Explicitly specified Rust types of [GraphQL objects][2] implementing this - /// [GraphQL interface][1] type. + /// Explicitly specified Rust types of [GraphQL objects][2] implementing + /// this [GraphQL interface][1] type. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces /// [2]: https://spec.graphql.org/June2018/#sec-Objects implementers: HashSet>, - /// Explicitly specified type of [`Context`] to use for resolving this [GraphQL interface][1] - /// type with. + /// Explicitly specified type of [`Context`] to use for resolving this + /// [GraphQL interface][1] type with. /// - /// If absent, then unit type `()` is assumed as type of [`Context`]. + /// If [`None`], then unit type `()` is assumed as a type of [`Context`]. /// /// [`Context`]: juniper::Context /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces context: Option>, - /// Explicitly specified type of [`ScalarValue`] to use for resolving this - /// [GraphQL interface][1] type with. + /// Explicitly specified type (or type parameter with its bounds) of + /// [`ScalarValue`] to resolve this [GraphQL interface][1] type with. /// - /// If absent, then generated code will be generic over any [`ScalarValue`] type, which, in - /// turn, requires all [interface][1] implementers to be generic over any [`ScalarValue`] type - /// too. That's why this type should be specified only if one of the implementers implements + /// If [`None`], then generated code will be generic over any + /// [`ScalarValue`] type, which, in turn, requires all [interface][1] + /// implementers to be generic over any [`ScalarValue`] type too. That's why + /// this type should be specified only if one of the implementers implements /// [`GraphQLType`] in a non-generic way over [`ScalarValue`] type. /// /// [`GraphQLType`]: juniper::GraphQLType /// [`ScalarValue`]: juniper::ScalarValue /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces - scalar: Option>, + scalar: Option>, - /// Explicitly specified marker indicating that the Rust trait should be transformed into - /// [`async_trait`]. + /// Explicitly specified marker indicating that the Rust trait should be + /// transformed into [`async_trait`]. /// - /// If absent, then trait will be transformed into [`async_trait`] only if it contains async - /// methods. + /// If [`None`], then trait will be transformed into [`async_trait`] only if + /// it contains async methods. asyncness: Option>, - /// Explicitly specified external downcasting functions for [GraphQL interface][1] implementers. + /// Explicitly specified external downcasting functions for + /// [GraphQL interface][1] implementers. /// - /// If absent, then macro will downcast to the implementers via enum dispatch or dynamic - /// dispatch (if the one is chosen). That's why specifying an external resolver function has - /// sense, when some custom [interface][1] implementer resolving logic is involved. + /// If [`None`], then macro will downcast to the implementers via enum + /// dispatch or dynamic dispatch (if the one is chosen). That's why + /// specifying an external resolver function has sense, when some custom + /// [interface][1] implementer resolving logic is involved. /// - /// Once the downcasting function is specified for some [GraphQL object][2] implementer type, it - /// cannot be downcast another such function or trait method marked with a - /// [`MethodMeta::downcast`] marker. + /// Once the downcasting function is specified for some [GraphQL object][2] + /// implementer type, it cannot be downcast another such function or trait + /// method marked with a [`MethodMeta::downcast`] marker. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces /// [2]: https://spec.graphql.org/June2018/#sec-Objects external_downcasts: HashMap>, - /// Indicator whether the generated code is intended to be used only inside the [`juniper`] - /// library. + /// Explicitly specified [`RenameRule`] for all fields of this + /// [GraphQL interface][1] type. + /// + /// If [`None`] then the default rule will be [`RenameRule::CamelCase`]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces + rename_fields: Option>, + + /// Indicator whether the generated code is intended to be used only inside + /// the [`juniper`] library. is_internal: bool, } -impl Parse for TraitMeta { - fn parse(input: ParseStream) -> syn::Result { - let mut output = Self::default(); - +impl Parse for TraitAttr { + fn parse(input: ParseStream<'_>) -> syn::Result { + let mut out = Self::default(); while !input.is_empty() { let ident = input.parse_any_ident()?; match ident.to_string().as_str() { "name" => { input.parse::()?; let name = input.parse::()?; - output - .name + out.name .replace(SpanContainer::new( ident.span(), Some(name.span()), @@ -143,8 +158,7 @@ impl Parse for TraitMeta { "desc" | "description" => { input.parse::()?; let desc = input.parse::()?; - output - .description + out.description .replace(SpanContainer::new( ident.span(), Some(desc.span()), @@ -155,16 +169,14 @@ impl Parse for TraitMeta { "ctx" | "context" | "Context" => { input.parse::()?; let ctx = input.parse::()?; - output - .context + out.context .replace(SpanContainer::new(ident.span(), Some(ctx.span()), ctx)) .none_or_else(|_| err::dup_arg(&ident))? } "scalar" | "Scalar" | "ScalarValue" => { input.parse::()?; - let scl = input.parse::()?; - output - .scalar + let scl = input.parse::()?; + out.scalar .replace(SpanContainer::new(ident.span(), Some(scl.span()), scl)) .none_or_else(|_| err::dup_arg(&ident))? } @@ -174,7 +186,7 @@ impl Parse for TraitMeta { syn::Type, token::Bracket, token::Comma, >()? { let impler_span = impler.span(); - output + out .implementers .replace(SpanContainer::new(ident.span(), Some(impler_span), impler)) .none_or_else(|_| err::dup_arg(impler_span))?; @@ -183,23 +195,20 @@ impl Parse for TraitMeta { "dyn" => { input.parse::()?; let alias = input.parse::()?; - output - .r#dyn + out.r#dyn .replace(SpanContainer::new(ident.span(), Some(alias.span()), alias)) .none_or_else(|_| err::dup_arg(&ident))? } "enum" => { input.parse::()?; let alias = input.parse::()?; - output - .r#enum + out.r#enum .replace(SpanContainer::new(ident.span(), Some(alias.span()), alias)) .none_or_else(|_| err::dup_arg(&ident))? } "async" => { let span = ident.span(); - output - .asyncness + out.asyncness .replace(SpanContainer::new(span, Some(span), ident)) .none_or_else(|_| err::dup_arg(span))?; } @@ -209,13 +218,23 @@ impl Parse for TraitMeta { let dwncst = input.parse::()?; let dwncst_spanned = SpanContainer::new(ident.span(), Some(ty.span()), dwncst); let dwncst_span = dwncst_spanned.span_joined(); - output - .external_downcasts + out.external_downcasts .insert(ty, dwncst_spanned) .none_or_else(|_| err::dup_arg(dwncst_span))? } + "rename_all" => { + input.parse::()?; + let val = input.parse::()?; + out.rename_fields + .replace(SpanContainer::new( + ident.span(), + Some(val.span()), + val.try_into()?, + )) + .none_or_else(|_| err::dup_arg(&ident))?; + } "internal" => { - output.is_internal = true; + out.is_internal = true; } name => { return Err(err::unknown_arg(&ident, name)); @@ -223,13 +242,13 @@ impl Parse for TraitMeta { } input.try_parse::()?; } - - Ok(output) + Ok(out) } } -impl TraitMeta { - /// Tries to merge two [`TraitMeta`]s into a single one, reporting about duplicates, if any. +impl TraitAttr { + /// Tries to merge two [`TraitAttr`]s into a single one, reporting about + /// duplicates, if any. fn try_merge(self, mut another: Self) -> syn::Result { Ok(Self { name: try_merge_opt!(name: self, another), @@ -243,19 +262,20 @@ impl TraitMeta { external_downcasts: try_merge_hashmap!( external_downcasts: self, another => span_joined ), + rename_fields: try_merge_opt!(rename_fields: self, another), is_internal: self.is_internal || another.is_internal, }) } - /// Parses [`TraitMeta`] from the given multiple `name`d [`syn::Attribute`]s placed on a trait - /// definition. + /// Parses [`TraitAttr`] from the given multiple `name`d [`syn::Attribute`]s + /// placed on a trait definition. fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result { - let mut meta = filter_attrs(name, attrs) + let mut attr = filter_attrs(name, attrs) .map(|attr| attr.parse_args()) .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?; - if let Some(as_dyn) = &meta.r#dyn { - if meta.r#enum.is_some() { + if let Some(as_dyn) = &attr.r#dyn { + if attr.r#enum.is_some() { return Err(syn::Error::new( as_dyn.span(), "`dyn` attribute argument is not composable with `enum` attribute argument", @@ -263,77 +283,76 @@ impl TraitMeta { } } - if meta.description.is_none() { - meta.description = get_doc_comment(attrs); + if attr.description.is_none() { + attr.description = get_doc_comment(attrs); } - Ok(meta) + Ok(attr) } } -/// Available metadata (arguments) behind `#[graphql_interface]` attribute placed on a trait -/// implementation block, when generating code for [GraphQL interface][1] type. +/// Available arguments behind `#[graphql_interface]` attribute placed on a +/// trait implementation block, when generating code for [GraphQL interface][1] +/// type. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces #[derive(Debug, Default)] -struct ImplMeta { - /// Explicitly specified type of [`ScalarValue`] to use for implementing the - /// [GraphQL interface][1] type. +struct ImplAttr { + /// Explicitly specified type (or type parameter with its bounds) of + /// [`ScalarValue`] to implementing the [GraphQL interface][1] type with. /// - /// If absent, then generated code will be generic over any [`ScalarValue`] type, which, in - /// turn, requires all [interface][1] implementers to be generic over any [`ScalarValue`] type - /// too. That's why this type should be specified only if the implementer itself implements - /// [`GraphQLType`] in a non-generic way over [`ScalarValue`] type. + /// If absent, then generated code will be generic over any [`ScalarValue`] + /// type, which, in turn, requires all [interface][1] implementers to be + /// generic over any [`ScalarValue`] type too. That's why this type should + /// be specified only if the implementer itself implements [`GraphQLType`] + /// in a non-generic way over [`ScalarValue`] type. /// /// [`GraphQLType`]: juniper::GraphQLType /// [`ScalarValue`]: juniper::ScalarValue /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces - scalar: Option>, + scalar: Option>, - /// Explicitly specified marker indicating that the trait implementation block should be - /// transformed with applying [`async_trait`]. + /// Explicitly specified marker indicating that the trait implementation + /// block should be transformed with applying [`async_trait`]. /// - /// If absent, then trait will be transformed with applying [`async_trait`] only if it contains - /// async methods. + /// If absent, then trait will be transformed with applying [`async_trait`] + /// only if it contains async methods. /// - /// This marker is especially useful when Rust trait contains async default methods, while the - /// implementation block doesn't. + /// This marker is especially useful when Rust trait contains async default + /// methods, while the implementation block doesn't. asyncness: Option>, - /// Explicitly specified marker indicating that the implemented [GraphQL interface][1] type is - /// represented as a [trait object][2] in Rust type system rather then an enum (default mode, - /// when the marker is absent). + /// Explicitly specified marker indicating that the implemented + /// [GraphQL interface][1] type is represented as a [trait object][2] in + /// Rust type system rather then an enum (default mode, when the marker is + /// absent). /// /// [2]: https://doc.rust-lang.org/reference/types/trait-object.html r#dyn: Option>, } -impl Parse for ImplMeta { - fn parse(input: ParseStream) -> syn::Result { - let mut output = Self::default(); - +impl Parse for ImplAttr { + fn parse(input: ParseStream<'_>) -> syn::Result { + let mut out = Self::default(); while !input.is_empty() { let ident = input.parse_any_ident()?; match ident.to_string().as_str() { "scalar" | "Scalar" | "ScalarValue" => { input.parse::()?; - let scl = input.parse::()?; - output - .scalar + let scl = input.parse::()?; + out.scalar .replace(SpanContainer::new(ident.span(), Some(scl.span()), scl)) .none_or_else(|_| err::dup_arg(&ident))? } "dyn" => { let span = ident.span(); - output - .r#dyn + out.r#dyn .replace(SpanContainer::new(span, Some(span), ident)) .none_or_else(|_| err::dup_arg(span))?; } "async" => { let span = ident.span(); - output - .asyncness + out.asyncness .replace(SpanContainer::new(span, Some(span), ident)) .none_or_else(|_| err::dup_arg(span))?; } @@ -343,13 +362,13 @@ impl Parse for ImplMeta { } input.try_parse::()?; } - - Ok(output) + Ok(out) } } -impl ImplMeta { - /// Tries to merge two [`ImplMeta`]s into a single one, reporting about duplicates, if any. +impl ImplAttr { + /// Tries to merge two [`ImplAttr`]s into a single one, reporting about + /// duplicates, if any. fn try_merge(self, mut another: Self) -> syn::Result { Ok(Self { scalar: try_merge_opt!(scalar: self, another), @@ -358,8 +377,8 @@ impl ImplMeta { }) } - /// Parses [`ImplMeta`] from the given multiple `name`d [`syn::Attribute`]s placed on a trait - /// implementation block. + /// Parses [`ImplAttr`] from the given multiple `name`d [`syn::Attribute`]s + /// placed on a trait implementation block. pub fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result { filter_attrs(name, attrs) .map(|attr| attr.parse_args()) @@ -367,344 +386,6 @@ impl ImplMeta { } } -/// Available metadata (arguments) behind `#[graphql]` attribute placed on a trait method -/// definition, when generating code for [GraphQL interface][1] type. -/// -/// [1]: https://spec.graphql.org/June2018/#sec-Interfaces -#[derive(Debug, Default)] -struct MethodMeta { - /// Explicitly specified name of a [GraphQL field][1] represented by this trait method. - /// - /// If absent, then `camelCased` Rust method name is used by default. - /// - /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields - name: Option>, - - /// Explicitly specified [description][2] of this [GraphQL field][1]. - /// - /// If absent, then Rust doc comment is used as the [description][2], if any. - /// - /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields - /// [2]: https://spec.graphql.org/June2018/#sec-Descriptions - description: Option>, - - /// Explicitly specified [deprecation][2] of this [GraphQL field][1]. - /// - /// If absent, then Rust `#[deprecated]` attribute is used as the [deprecation][2], if any. - /// - /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields - /// [2]: https://spec.graphql.org/June2018/#sec-Deprecation - deprecated: Option>>, - - /// Explicitly specified marker indicating that this trait method should be omitted by code - /// generation and not considered in the [GraphQL interface][1] type definition. - /// - /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces - ignore: Option>, - - /// Explicitly specified marker indicating that this trait method doesn't represent a - /// [GraphQL field][1], but is a downcasting function into the [GraphQL object][2] implementer - /// type returned by this trait method. - /// - /// Once this marker is specified, the [GraphQL object][2] implementer type cannot be downcast - /// via another trait method or [`TraitMeta::external_downcasts`] function. - /// - /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields - /// [2]: https://spec.graphql.org/June2018/#sec-Objects - downcast: Option>, -} - -impl Parse for MethodMeta { - fn parse(input: ParseStream) -> syn::Result { - let mut output = Self::default(); - - while !input.is_empty() { - let ident = input.parse::()?; - match ident.to_string().as_str() { - "name" => { - input.parse::()?; - let name = input.parse::()?; - output - .name - .replace(SpanContainer::new(ident.span(), Some(name.span()), name)) - .none_or_else(|_| err::dup_arg(&ident))? - } - "desc" | "description" => { - input.parse::()?; - let desc = input.parse::()?; - output - .description - .replace(SpanContainer::new(ident.span(), Some(desc.span()), desc)) - .none_or_else(|_| err::dup_arg(&ident))? - } - "deprecated" => { - let mut reason = None; - if input.is_next::() { - input.parse::()?; - reason = Some(input.parse::()?); - } - output - .deprecated - .replace(SpanContainer::new( - ident.span(), - reason.as_ref().map(|r| r.span()), - reason, - )) - .none_or_else(|_| err::dup_arg(&ident))? - } - "ignore" | "skip" => output - .ignore - .replace(SpanContainer::new(ident.span(), None, ident.clone())) - .none_or_else(|_| err::dup_arg(&ident))?, - "downcast" => output - .downcast - .replace(SpanContainer::new(ident.span(), None, ident.clone())) - .none_or_else(|_| err::dup_arg(&ident))?, - name => { - return Err(err::unknown_arg(&ident, name)); - } - } - input.try_parse::()?; - } - - Ok(output) - } -} - -impl MethodMeta { - /// Tries to merge two [`MethodMeta`]s into a single one, reporting about duplicates, if any. - fn try_merge(self, mut another: Self) -> syn::Result { - Ok(Self { - name: try_merge_opt!(name: self, another), - description: try_merge_opt!(description: self, another), - deprecated: try_merge_opt!(deprecated: self, another), - ignore: try_merge_opt!(ignore: self, another), - downcast: try_merge_opt!(downcast: self, another), - }) - } - - /// Parses [`MethodMeta`] from the given multiple `name`d [`syn::Attribute`]s placed on a - /// method definition. - pub fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result { - let mut meta = filter_attrs(name, attrs) - .map(|attr| attr.parse_args()) - .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?; - - if let Some(ignore) = &meta.ignore { - if meta.name.is_some() - || meta.description.is_some() - || meta.deprecated.is_some() - || meta.downcast.is_some() - { - return Err(syn::Error::new( - ignore.span(), - "`ignore` attribute argument is not composable with any other arguments", - )); - } - } - - if let Some(downcast) = &meta.downcast { - if meta.name.is_some() - || meta.description.is_some() - || meta.deprecated.is_some() - || meta.ignore.is_some() - { - return Err(syn::Error::new( - downcast.span(), - "`downcast` attribute argument is not composable with any other arguments", - )); - } - } - - if meta.description.is_none() { - meta.description = get_doc_comment(attrs).map(|sc| { - let span = sc.span_ident(); - sc.map(|desc| syn::LitStr::new(&desc, span)) - }); - } - - if meta.deprecated.is_none() { - meta.deprecated = get_deprecated(attrs).map(|sc| { - let span = sc.span_ident(); - sc.map(|depr| depr.reason.map(|rsn| syn::LitStr::new(&rsn, span))) - }); - } - - Ok(meta) - } -} - -/// Available metadata (arguments) behind `#[graphql]` attribute placed on a trait method argument, -/// when generating code for [GraphQL interface][1] type. -/// -/// [1]: https://spec.graphql.org/June2018/#sec-Interfaces -#[derive(Debug, Default)] -struct ArgumentMeta { - /// Explicitly specified name of a [GraphQL argument][1] represented by this method argument. - /// - /// If absent, then `camelCased` Rust argument name is used by default. - /// - /// [1]: https://spec.graphql.org/June2018/#sec-Language.Arguments - name: Option>, - - /// Explicitly specified [description][2] of this [GraphQL argument][1]. - /// - /// [1]: https://spec.graphql.org/June2018/#sec-Language.Arguments - /// [2]: https://spec.graphql.org/June2018/#sec-Descriptions - description: Option>, - - /// Explicitly specified [default value][2] of this [GraphQL argument][1]. - /// - /// If the exact default expression is not specified, then the [`Default::default`] value is - /// used. - /// - /// If absent, then this [GraphQL argument][1] is considered as [required][2]. - /// - /// [1]: https://spec.graphql.org/June2018/#sec-Language.Arguments - /// [2]: https://spec.graphql.org/June2018/#sec-Required-Arguments - default: Option>>, - - /// Explicitly specified marker indicating that this method argument doesn't represent a - /// [GraphQL argument][1], but is a [`Context`] being injected into a [GraphQL field][2] - /// resolving function. - /// - /// If absent, then the method argument still is considered as [`Context`] if it's named - /// `context` or `ctx`. - /// - /// [`Context`]: juniper::Context - /// [1]: https://spec.graphql.org/June2018/#sec-Language.Arguments - /// [2]: https://spec.graphql.org/June2018/#sec-Language.Fields - context: Option>, - - /// Explicitly specified marker indicating that this method argument doesn't represent a - /// [GraphQL argument][1], but is a [`Executor`] being injected into a [GraphQL field][2] - /// resolving function. - /// - /// If absent, then the method argument still is considered as [`Context`] if it's named - /// `executor`. - /// - /// [`Executor`]: juniper::Executor - /// [1]: https://spec.graphql.org/June2018/#sec-Language.Arguments - /// [2]: https://spec.graphql.org/June2018/#sec-Language.Fields - executor: Option>, -} - -impl Parse for ArgumentMeta { - fn parse(input: ParseStream) -> syn::Result { - let mut output = Self::default(); - - while !input.is_empty() { - let ident = input.parse::()?; - match ident.to_string().as_str() { - "name" => { - input.parse::()?; - let name = input.parse::()?; - output - .name - .replace(SpanContainer::new(ident.span(), Some(name.span()), name)) - .none_or_else(|_| err::dup_arg(&ident))? - } - "desc" | "description" => { - input.parse::()?; - let desc = input.parse::()?; - output - .description - .replace(SpanContainer::new(ident.span(), Some(desc.span()), desc)) - .none_or_else(|_| err::dup_arg(&ident))? - } - "default" => { - let mut expr = None; - if input.is_next::() { - input.parse::()?; - expr = Some(input.parse::()?); - } else if input.is_next::() { - let inner; - let _ = syn::parenthesized!(inner in input); - expr = Some(inner.parse::()?); - } - output - .default - .replace(SpanContainer::new( - ident.span(), - expr.as_ref().map(|e| e.span()), - expr, - )) - .none_or_else(|_| err::dup_arg(&ident))? - } - "ctx" | "context" | "Context" => { - let span = ident.span(); - output - .context - .replace(SpanContainer::new(span, Some(span), ident)) - .none_or_else(|_| err::dup_arg(span))? - } - "exec" | "executor" => { - let span = ident.span(); - output - .executor - .replace(SpanContainer::new(span, Some(span), ident)) - .none_or_else(|_| err::dup_arg(span))? - } - name => { - return Err(err::unknown_arg(&ident, name)); - } - } - input.try_parse::()?; - } - - Ok(output) - } -} - -impl ArgumentMeta { - /// Tries to merge two [`ArgumentMeta`]s into a single one, reporting about duplicates, if any. - fn try_merge(self, mut another: Self) -> syn::Result { - Ok(Self { - name: try_merge_opt!(name: self, another), - description: try_merge_opt!(description: self, another), - default: try_merge_opt!(default: self, another), - context: try_merge_opt!(context: self, another), - executor: try_merge_opt!(executor: self, another), - }) - } - - /// Parses [`ArgumentMeta`] from the given multiple `name`d [`syn::Attribute`]s placed on a - /// function argument. - fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result { - let meta = filter_attrs(name, attrs) - .map(|attr| attr.parse_args()) - .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?; - - if let Some(context) = &meta.context { - if meta.name.is_some() - || meta.description.is_some() - || meta.default.is_some() - || meta.executor.is_some() - { - return Err(syn::Error::new( - context.span(), - "`context` attribute argument is not composable with any other arguments", - )); - } - } - - if let Some(executor) = &meta.executor { - if meta.name.is_some() - || meta.description.is_some() - || meta.default.is_some() - || meta.context.is_some() - { - return Err(syn::Error::new( - executor.span(), - "`executor` attribute argument is not composable with any other arguments", - )); - } - } - - Ok(meta) - } -} - /// Definition of [GraphQL interface][1] for code generation. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces @@ -724,28 +405,27 @@ struct Definition { /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces description: Option, - /// Rust type of [`Context`] to generate [`GraphQLType`] implementation with for this - /// [GraphQL interface][1]. - /// - /// If [`None`] then generated code will use unit type `()` as [`Context`]. + /// Rust type of [`Context`] to generate [`GraphQLType`] implementation with + /// for this [GraphQL interface][1]. /// /// [`GraphQLType`]: juniper::GraphQLType /// [`Context`]: juniper::Context /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces - context: Option, + context: syn::Type, - /// [`ScalarValue`] parametrization to generate [`GraphQLType`] implementation with for this - /// [GraphQL interface][1]. + /// [`ScalarValue`] parametrization to generate [`GraphQLType`] + /// implementation with for this [GraphQL interface][1]. /// /// [`GraphQLType`]: juniper::GraphQLType /// [`ScalarValue`]: juniper::ScalarValue /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces - scalar: ScalarValueType, + scalar: scalar::Type, - /// Defined [`Field`]s of this [GraphQL interface][1]. + /// Defined [GraphQL fields][2] of this [GraphQL interface][1]. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces - fields: Vec, + /// [2]: https://spec.graphql.org/June2018/#sec-Language.Fields + fields: Vec, /// Defined [`Implementer`]s of this [GraphQL interface][1]. /// @@ -753,635 +433,298 @@ struct Definition { implementers: Vec, } -impl Definition { - /// Returns generated code that panics about unknown field tried to be resolved on this - /// [GraphQL interface][1]. - /// - /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces - #[must_use] - fn panic_no_field_tokens(&self) -> TokenStream { - let scalar = &self.scalar; - - quote! { - panic!( - "Field `{}` not found on type `{}`", - field, - >::name(info).unwrap(), - ) - } +impl ToTokens for Definition { + fn to_tokens(&self, into: &mut TokenStream) { + self.ty.to_token_stream().to_tokens(into); + self.impl_graphql_interface_tokens().to_tokens(into); + self.impl_output_type_tokens().to_tokens(into); + self.impl_graphql_type_tokens().to_tokens(into); + self.impl_graphql_value_tokens().to_tokens(into); + self.impl_graphql_value_async_tokens().to_tokens(into); } +} - /// Returns generated code implementing [`GraphQLType`] trait for this [GraphQL interface][1]. +impl Definition { + /// Returns generated code implementing [`GraphQLInterface`] trait for this + /// [GraphQL interface][1]. /// - /// [`GraphQLType`]: juniper::GraphQLType + /// [`GraphQLInterface`]: juniper::GraphQLInterface /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces #[must_use] - fn impl_graphql_type_tokens(&self) -> TokenStream { + fn impl_graphql_interface_tokens(&self) -> TokenStream { let scalar = &self.scalar; - let generics = self.ty.impl_generics(); - let (impl_generics, _, where_clause) = generics.split_for_impl(); - + let (impl_generics, where_clause) = self.ty.impl_generics(false); let ty = self.ty.ty_tokens(); - let name = &self.name; - let description = self - .description - .as_ref() - .map(|desc| quote! { .description(#desc) }); - - // Sorting is required to preserve/guarantee the order of implementers registered in schema. - let mut impler_tys: Vec<_> = self.implementers.iter().map(|impler| &impler.ty).collect(); - impler_tys.sort_unstable_by(|a, b| { - let (a, b) = (quote!(#a).to_string(), quote!(#b).to_string()); - a.cmp(&b) + let impler_tys: Vec<_> = self.implementers.iter().map(|impler| &impler.ty).collect(); + let all_implers_unique = (impler_tys.len() > 1).then(|| { + quote! { ::juniper::sa::assert_type_ne_all!(#( #impler_tys ),*); } }); - let fields_meta = self.fields.iter().map(Field::method_meta_tokens); - quote! { #[automatically_derived] - impl#impl_generics ::juniper::GraphQLType<#scalar> for #ty #where_clause + impl#impl_generics ::juniper::marker::GraphQLInterface<#scalar> for #ty #where_clause { - fn name(_ : &Self::TypeInfo) -> Option<&'static str> { - Some(#name) - } - - fn meta<'r>( - info: &Self::TypeInfo, - registry: &mut ::juniper::Registry<'r, #scalar> - ) -> ::juniper::meta::MetaType<'r, #scalar> - where #scalar: 'r, - { - // Ensure all implementer types are registered. - #( let _ = registry.get_type::<#impler_tys>(info); )* - - let fields = [ - #( #fields_meta, )* - ]; - registry.build_interface_type::<#ty>(info, &fields) - #description - .into_meta() + fn mark() { + #all_implers_unique + #( <#impler_tys as ::juniper::marker::GraphQLObject<#scalar>>::mark(); )* } } } } - /// Returns generated code implementing [`GraphQLValue`] trait for this [GraphQL interface][1]. + /// Returns generated code implementing [`marker::IsOutputType`] trait for + /// this [GraphQL interface][1]. /// - /// [`GraphQLValue`]: juniper::GraphQLValue + /// [`marker::IsOutputType`]: juniper::marker::IsOutputType /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces #[must_use] - fn impl_graphql_value_tokens(&self) -> TokenStream { + fn impl_output_type_tokens(&self) -> TokenStream { let scalar = &self.scalar; - let generics = self.ty.impl_generics(); - let (impl_generics, _, where_clause) = generics.split_for_impl(); - - let ty = self.ty.ty_tokens(); - let trait_ty = self.ty.trait_ty(); - let context = self.context.clone().unwrap_or_else(|| parse_quote! { () }); - - let fields_resolvers = self - .fields - .iter() - .filter_map(|f| f.method_resolve_field_tokens(&trait_ty)); - let async_fields_panic = { - let names = self - .fields - .iter() - .filter_map(|field| { - if field.is_async { - Some(&field.name) - } else { - None - } - }) - .collect::>(); - if names.is_empty() { - None - } else { - Some(quote! { - #( #names )|* => panic!( - "Tried to resolve async field `{}` on type `{}` with a sync resolver", - field, - >::name(info).unwrap(), - ), - }) - } - }; - let no_field_panic = self.panic_no_field_tokens(); - - let custom_downcast_checks = self - .implementers - .iter() - .filter_map(|i| i.method_concrete_type_name_tokens(&trait_ty)); - let regular_downcast_check = self.ty.method_concrete_type_name_tokens(); - - let custom_downcasts = self - .implementers - .iter() - .filter_map(|i| i.method_resolve_into_type_tokens(&trait_ty)); - let regular_downcast = self.ty.method_resolve_into_type_tokens(); - - quote! { - #[automatically_derived] - impl#impl_generics ::juniper::GraphQLValue<#scalar> for #ty #where_clause - { - type Context = #context; - type TypeInfo = (); - - fn type_name<'__i>(&self, info: &'__i Self::TypeInfo) -> Option<&'__i str> { - >::name(info) - } - - fn resolve_field( - &self, - info: &Self::TypeInfo, - field: &str, - args: &::juniper::Arguments<#scalar>, - executor: &::juniper::Executor, - ) -> ::juniper::ExecutionResult<#scalar> { - match field { - #( #fields_resolvers )* - #async_fields_panic - _ => #no_field_panic, - } - } - - fn concrete_type_name( - &self, - context: &Self::Context, - info: &Self::TypeInfo, - ) -> String { - #( #custom_downcast_checks )* - #regular_downcast_check - } - - fn resolve_into_type( - &self, - info: &Self::TypeInfo, - type_name: &str, - _: Option<&[::juniper::Selection<#scalar>]>, - executor: &::juniper::Executor, - ) -> ::juniper::ExecutionResult<#scalar> { - #( #custom_downcasts )* - #regular_downcast - } - } - } - } - - /// Returns generated code implementing [`GraphQLValueAsync`] trait for this - /// [GraphQL interface][1]. - /// - /// [`GraphQLValueAsync`]: juniper::GraphQLValueAsync - /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces - #[must_use] - fn impl_graphql_value_async_tokens(&self) -> TokenStream { - let scalar = &self.scalar; - - let generics = self.ty.impl_generics(); - let (impl_generics, _, where_clause) = generics.split_for_impl(); - let mut where_clause = where_clause - .cloned() - .unwrap_or_else(|| parse_quote! { where }); - where_clause.predicates.push(parse_quote! { Self: Sync }); - if self.scalar.is_generic() { - where_clause - .predicates - .push(parse_quote! { #scalar: Send + Sync }); - } - + let (impl_generics, where_clause) = self.ty.impl_generics(false); let ty = self.ty.ty_tokens(); - let trait_ty = self.ty.trait_ty(); - let fields_resolvers = self + let fields_marks = self .fields .iter() - .map(|f| f.method_resolve_field_async_tokens(&trait_ty)); - let no_field_panic = self.panic_no_field_tokens(); - - let custom_downcasts = self - .implementers - .iter() - .filter_map(|i| i.method_resolve_into_type_async_tokens(&trait_ty)); - let regular_downcast = self.ty.method_resolve_into_type_async_tokens(); - - quote! { - #[automatically_derived] - impl#impl_generics ::juniper::GraphQLValueAsync<#scalar> for #ty #where_clause - { - fn resolve_field_async<'b>( - &'b self, - info: &'b Self::TypeInfo, - field: &'b str, - args: &'b ::juniper::Arguments<#scalar>, - executor: &'b ::juniper::Executor, - ) -> ::juniper::BoxFuture<'b, ::juniper::ExecutionResult<#scalar>> { - match field { - #( #fields_resolvers )* - _ => #no_field_panic, - } - } - - fn resolve_into_type_async<'b>( - &'b self, - info: &'b Self::TypeInfo, - type_name: &str, - _: Option<&'b [::juniper::Selection<'b, #scalar>]>, - executor: &'b ::juniper::Executor<'b, 'b, Self::Context, #scalar> - ) -> ::juniper::BoxFuture<'b, ::juniper::ExecutionResult<#scalar>> { - #( #custom_downcasts )* - #regular_downcast - } - } - } - } - - /// Returns generated code implementing [`GraphQLInterface`] trait for this - /// [GraphQL interface][1]. - /// - /// [`GraphQLInterface`]: juniper::GraphQLInterface - /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces - #[must_use] - fn impl_graphql_interface_tokens(&self) -> TokenStream { - let scalar = &self.scalar; - - let generics = self.ty.impl_generics(); - let (impl_generics, _, where_clause) = generics.split_for_impl(); - - let ty = self.ty.ty_tokens(); - - let impler_tys: Vec<_> = self.implementers.iter().map(|impler| &impler.ty).collect(); - - let all_implers_unique = if impler_tys.len() > 1 { - Some(quote! { ::juniper::sa::assert_type_ne_all!(#( #impler_tys ),*); }) - } else { - None - }; - - quote! { - #[automatically_derived] - impl#impl_generics ::juniper::marker::GraphQLInterface<#scalar> for #ty #where_clause - { - fn mark() { - #all_implers_unique - - #( <#impler_tys as ::juniper::marker::GraphQLObjectType<#scalar>>::mark(); )* - } - } - } - } - - /// Returns generated code implementing [`marker::IsOutputType`] trait for this - /// [GraphQL interface][1]. - /// - /// [`marker::IsOutputType`]: juniper::marker::IsOutputType - /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces - #[must_use] - fn impl_output_type_tokens(&self) -> TokenStream { - let scalar = &self.scalar; - - let generics = self.ty.impl_generics(); - let (impl_generics, _, where_clause) = generics.split_for_impl(); - - let ty = self.ty.ty_tokens(); - - let fields_marks = self.fields.iter().map(|field| { - let arguments_marks = field.arguments.iter().filter_map(|arg| { - let arg_ty = &arg.as_regular()?.ty; - Some(quote! { <#arg_ty as ::juniper::marker::IsInputType<#scalar>>::mark(); }) - }); - - let field_ty = &field.ty; - let resolved_ty = quote! { - <#field_ty as ::juniper::IntoResolvable< - '_, #scalar, _, >::Context, - >>::Type - }; - - quote! { - #( #arguments_marks )* - <#resolved_ty as ::juniper::marker::IsOutputType<#scalar>>::mark(); - } - }); - - let impler_tys = self.implementers.iter().map(|impler| &impler.ty); - - quote! { - #[automatically_derived] - impl#impl_generics ::juniper::marker::IsOutputType<#scalar> for #ty #where_clause - { - fn mark() { - #( #fields_marks )* - #( <#impler_tys as ::juniper::marker::IsOutputType<#scalar>>::mark(); )* - } - } - } - } -} - -impl ToTokens for Definition { - fn to_tokens(&self, into: &mut TokenStream) { - into.append_all(&[ - self.ty.to_token_stream(), - self.impl_graphql_interface_tokens(), - self.impl_output_type_tokens(), - self.impl_graphql_type_tokens(), - self.impl_graphql_value_tokens(), - self.impl_graphql_value_async_tokens(), - ]); - } -} - -/// Representation of [GraphQL interface][1] field [argument][2] for code generation. -/// -/// [1]: https://spec.graphql.org/June2018/#sec-Interfaces -/// [2]: https://spec.graphql.org/June2018/#sec-Language.Arguments -#[derive(Debug)] -struct FieldArgument { - /// Rust type that this [GraphQL field argument][2] is represented by. - /// - /// [2]: https://spec.graphql.org/June2018/#sec-Language.Arguments - ty: syn::Type, - - /// Name of this [GraphQL field argument][2] in GraphQL schema. - /// - /// [2]: https://spec.graphql.org/June2018/#sec-Language.Arguments - name: String, - - /// [Description][1] of this [GraphQL field argument][2] to put into GraphQL schema. - /// - /// [1]: https://spec.graphql.org/June2018/#sec-Descriptions - /// [2]: https://spec.graphql.org/June2018/#sec-Language.Arguments - description: Option, - - /// Default value of this [GraphQL field argument][2] in GraphQL schema. - /// - /// If outer [`Option`] is [`None`], then this [argument][2] is a [required][3] one. - /// - /// If inner [`Option`] is [`None`], then the [`Default::default`] value is used. - /// - /// [2]: https://spec.graphql.org/June2018/#sec-Language.Arguments - /// [3]: https://spec.graphql.org/June2018/#sec-Required-Arguments - default: Option>, -} - -/// Possible kinds of Rust trait method arguments for code generation. -#[derive(Debug)] -enum MethodArgument { - /// Regular [GraphQL field argument][1]. - /// - /// [1]: https://spec.graphql.org/June2018/#sec-Language.Arguments - Regular(FieldArgument), - - /// [`Context`] passed into a [GraphQL field][2] resolving method. - /// - /// [`Context`]: juniper::Context - /// [2]: https://spec.graphql.org/June2018/#sec-Language.Fields - Context(syn::Type), - - /// [`Executor`] passed into a [GraphQL field][2] resolving method. - /// - /// [`Executor`]: juniper::Executor - /// [2]: https://spec.graphql.org/June2018/#sec-Language.Fields - Executor, -} - -impl MethodArgument { - /// Returns this [`MethodArgument`] as a [`FieldArgument`], if it represents one. - #[must_use] - fn as_regular(&self) -> Option<&FieldArgument> { - if let Self::Regular(arg) = self { - Some(arg) - } else { - None - } - } - - /// Returns [`syn::Type`] of this [`MethodArgument::Context`], if it represents one. - #[must_use] - fn context_ty(&self) -> Option<&syn::Type> { - if let Self::Context(ty) = self { - Some(ty) - } else { - None - } - } - - /// Returns generated code for the [`GraphQLType::meta`] method, which registers this - /// [`MethodArgument`] in [`Registry`], if it represents a [`FieldArgument`]. - /// - /// [`GraphQLType::meta`]: juniper::GraphQLType::meta - /// [`Registry`]: juniper::Registry - #[must_use] - fn method_meta_tokens(&self) -> Option { - let arg = self.as_regular()?; - - let (name, ty) = (&arg.name, &arg.ty); - - let description = arg - .description - .as_ref() - .map(|desc| quote! { .description(#desc) }); - - let method = if let Some(val) = &arg.default { - let val = val - .as_ref() - .map(|v| quote! { (#v).into() }) - .unwrap_or_else(|| quote! { <#ty as Default>::default() }); - quote! { .arg_with_default::<#ty>(#name, &#val, info) } - } else { - quote! { .arg::<#ty>(#name, info) } - }; - - Some(quote! { .argument(registry#method#description) }) - } - - /// Returns generated code for the [`GraphQLValue::resolve_field`] method, which provides the - /// value of this [`MethodArgument`] to be passed into a trait method call. - /// - /// [`GraphQLValue::resolve_field`]: juniper::GraphQLValue::resolve_field - #[must_use] - fn method_resolve_field_tokens(&self) -> TokenStream { - match self { - Self::Regular(arg) => { - let (name, ty) = (&arg.name, &arg.ty); - let err_text = format!( - "Internal error: missing argument `{}` - validation must have failed", - &name, - ); - - quote! { - args.get::<#ty>(#name).expect(#err_text) - } - } - - Self::Context(_) => quote! { - ::juniper::FromContext::from(executor.context()) - }, - - Self::Executor => quote! { &executor }, - } - } -} - -/// Representation of [GraphQL interface][1] [field][2] for code generation. -/// -/// [1]: https://spec.graphql.org/June2018/#sec-Interfaces -/// [2]: https://spec.graphql.org/June2018/#sec-Language.Fields -#[derive(Debug)] -struct Field { - /// Rust type that this [GraphQL field][2] is represented by (method return type). - /// - /// [2]: https://spec.graphql.org/June2018/#sec-Language.Fields - ty: syn::Type, - - /// Name of this [GraphQL field][2] in GraphQL schema. - /// - /// [2]: https://spec.graphql.org/June2018/#sec-Language.Fields - name: String, - - /// [Description][1] of this [GraphQL field][2] to put into GraphQL schema. - /// - /// [1]: https://spec.graphql.org/June2018/#sec-Descriptions - /// [2]: https://spec.graphql.org/June2018/#sec-Language.Fields - description: Option, - - /// [Deprecation][1] of this [GraphQL field][2] to put into GraphQL schema. - /// - /// If inner [`Option`] is [`None`], then deprecation has no message attached. - /// - /// [1]: https://spec.graphql.org/June2018/#sec-Deprecation - /// [2]: https://spec.graphql.org/June2018/#sec-Language.Fields - deprecated: Option>, - - /// Name of Rust trait method representing this [GraphQL field][2]. - /// - /// [2]: https://spec.graphql.org/June2018/#sec-Language.Fields - method: syn::Ident, + .map(|f| f.method_mark_tokens(false, scalar)); - /// Rust trait [`MethodArgument`]s required to call the trait method representing this - /// [GraphQL field][2]. - /// - /// [2]: https://spec.graphql.org/June2018/#sec-Language.Fields - arguments: Vec, + let impler_tys = self.implementers.iter().map(|impler| &impler.ty); - /// Indicator whether this [GraphQL field][2] should be resolved asynchronously. - /// - /// [2]: https://spec.graphql.org/June2018/#sec-Language.Fields - is_async: bool, -} + quote! { + #[automatically_derived] + impl#impl_generics ::juniper::marker::IsOutputType<#scalar> for #ty #where_clause + { + fn mark() { + #( #fields_marks )* + #( <#impler_tys as ::juniper::marker::IsOutputType<#scalar>>::mark(); )* + } + } + } + } -impl Field { - /// Returns generated code for the [`GraphQLType::meta`] method, which registers this - /// [`Field`] in [`Registry`]. + /// Returns generated code implementing [`GraphQLType`] trait for this + /// [GraphQL interface][1]. /// - /// [`GraphQLType::meta`]: juniper::GraphQLType::meta - /// [`Registry`]: juniper::Registry + /// [`GraphQLType`]: juniper::GraphQLType + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces #[must_use] - fn method_meta_tokens(&self) -> TokenStream { - let (name, ty) = (&self.name, &self.ty); + fn impl_graphql_type_tokens(&self) -> TokenStream { + let scalar = &self.scalar; + + let (impl_generics, where_clause) = self.ty.impl_generics(false); + let ty = self.ty.ty_tokens(); + let name = &self.name; let description = self .description .as_ref() .map(|desc| quote! { .description(#desc) }); - let deprecated = self.deprecated.as_ref().map(|reason| { - let reason = reason - .as_ref() - .map(|rsn| quote! { Some(#rsn) }) - .unwrap_or_else(|| quote! { None }); - quote! { .deprecated(#reason) } + // Sorting is required to preserve/guarantee the order of implementers registered in schema. + let mut impler_tys: Vec<_> = self.implementers.iter().map(|impler| &impler.ty).collect(); + impler_tys.sort_unstable_by(|a, b| { + let (a, b) = (quote!(#a).to_string(), quote!(#b).to_string()); + a.cmp(&b) }); - let arguments = self - .arguments - .iter() - .filter_map(MethodArgument::method_meta_tokens); + let fields_meta = self.fields.iter().map(|f| f.method_meta_tokens(None)); quote! { - registry.field_convert::<#ty, _, Self::Context>(#name, info) - #( #arguments )* - #description - #deprecated + #[automatically_derived] + impl#impl_generics ::juniper::GraphQLType<#scalar> for #ty #where_clause + { + fn name(_ : &Self::TypeInfo) -> Option<&'static str> { + Some(#name) + } + + fn meta<'r>( + info: &Self::TypeInfo, + registry: &mut ::juniper::Registry<'r, #scalar> + ) -> ::juniper::meta::MetaType<'r, #scalar> + where #scalar: 'r, + { + // Ensure all implementer types are registered. + #( let _ = registry.get_type::<#impler_tys>(info); )* + + let fields = [ + #( #fields_meta, )* + ]; + registry.build_interface_type::<#ty>(info, &fields) + #description + .into_meta() + } + } } } - /// Returns generated code for the [`GraphQLValue::resolve_field`] method, which resolves this - /// [`Field`] synchronously. - /// - /// Returns [`None`] if this [`Field::is_async`]. + /// Returns generated code implementing [`GraphQLValue`] trait for this + /// [GraphQL interface][1]. /// - /// [`GraphQLValue::resolve_field`]: juniper::GraphQLValue::resolve_field + /// [`GraphQLValue`]: juniper::GraphQLValue + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces #[must_use] - fn method_resolve_field_tokens(&self, trait_ty: &syn::Type) -> Option { - if self.is_async { - return None; - } + fn impl_graphql_value_tokens(&self) -> TokenStream { + let scalar = &self.scalar; + let context = &self.context; + + let (impl_generics, where_clause) = self.ty.impl_generics(false); + let ty = self.ty.ty_tokens(); + let trait_ty = self.ty.trait_ty(); + + let fields_resolvers = self + .fields + .iter() + .filter_map(|f| f.method_resolve_field_tokens(scalar, Some(&trait_ty))); + let async_fields_panic = { + let names = self + .fields + .iter() + .filter_map(|f| f.is_async.then(|| f.name.as_str())) + .collect::>(); + (!names.is_empty()).then(|| { + field::Definition::method_resolve_field_panic_async_field_tokens(&names, scalar) + }) + }; + let no_field_panic = field::Definition::method_resolve_field_panic_no_field_tokens(scalar); - let (name, ty, method) = (&self.name, &self.ty, &self.method); + let custom_downcast_checks = self + .implementers + .iter() + .filter_map(|i| i.method_concrete_type_name_tokens(&trait_ty)); + let regular_downcast_check = self.ty.method_concrete_type_name_tokens(); - let arguments = self - .arguments + let custom_downcasts = self + .implementers .iter() - .map(MethodArgument::method_resolve_field_tokens); + .filter_map(|i| i.method_resolve_into_type_tokens(&trait_ty)); + let regular_downcast = self.ty.method_resolve_into_type_tokens(); - let resolving_code = gen::sync_resolving_code(); + quote! { + #[allow(deprecated)] + #[automatically_derived] + impl#impl_generics ::juniper::GraphQLValue<#scalar> for #ty #where_clause + { + type Context = #context; + type TypeInfo = (); - Some(quote! { - #name => { - let res: #ty = ::#method(self #( , #arguments )*); - #resolving_code + fn type_name<'__i>(&self, info: &'__i Self::TypeInfo) -> Option<&'__i str> { + >::name(info) + } + + fn resolve_field( + &self, + info: &Self::TypeInfo, + field: &str, + args: &::juniper::Arguments<#scalar>, + executor: &::juniper::Executor, + ) -> ::juniper::ExecutionResult<#scalar> { + match field { + #( #fields_resolvers )* + #async_fields_panic + _ => #no_field_panic, + } + } + + fn concrete_type_name( + &self, + context: &Self::Context, + info: &Self::TypeInfo, + ) -> String { + #( #custom_downcast_checks )* + #regular_downcast_check + } + + fn resolve_into_type( + &self, + info: &Self::TypeInfo, + type_name: &str, + _: Option<&[::juniper::Selection<#scalar>]>, + executor: &::juniper::Executor, + ) -> ::juniper::ExecutionResult<#scalar> { + #( #custom_downcasts )* + #regular_downcast + } } - }) + } } - /// Returns generated code for the [`GraphQLValueAsync::resolve_field_async`] method, which - /// resolves this [`Field`] asynchronously. + /// Returns generated code implementing [`GraphQLValueAsync`] trait for this + /// [GraphQL interface][1]. /// - /// [`GraphQLValueAsync::resolve_field_async`]: juniper::GraphQLValueAsync::resolve_field_async + /// [`GraphQLValueAsync`]: juniper::GraphQLValueAsync + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces #[must_use] - fn method_resolve_field_async_tokens(&self, trait_ty: &syn::Type) -> TokenStream { - let (name, ty, method) = (&self.name, &self.ty, &self.method); + fn impl_graphql_value_async_tokens(&self) -> TokenStream { + let scalar = &self.scalar; - let arguments = self - .arguments - .iter() - .map(MethodArgument::method_resolve_field_tokens); + let (impl_generics, where_clause) = self.ty.impl_generics(true); + let ty = self.ty.ty_tokens(); + let trait_ty = self.ty.trait_ty(); - let mut fut = quote! { ::#method(self #( , #arguments )*) }; - if !self.is_async { - fut = quote! { ::juniper::futures::future::ready(#fut) }; - } + let fields_resolvers = self + .fields + .iter() + .map(|f| f.method_resolve_field_async_tokens(scalar, Some(&trait_ty))); + let no_field_panic = field::Definition::method_resolve_field_panic_no_field_tokens(scalar); - let resolving_code = gen::async_resolving_code(Some(ty)); + let custom_downcasts = self + .implementers + .iter() + .filter_map(|i| i.method_resolve_into_type_async_tokens(&trait_ty)); + let regular_downcast = self.ty.method_resolve_into_type_async_tokens(); quote! { - #name => { - let fut = #fut; - #resolving_code + #[allow(deprecated, non_snake_case)] + #[automatically_derived] + impl#impl_generics ::juniper::GraphQLValueAsync<#scalar> for #ty #where_clause + { + fn resolve_field_async<'b>( + &'b self, + info: &'b Self::TypeInfo, + field: &'b str, + args: &'b ::juniper::Arguments<#scalar>, + executor: &'b ::juniper::Executor, + ) -> ::juniper::BoxFuture<'b, ::juniper::ExecutionResult<#scalar>> { + match field { + #( #fields_resolvers )* + _ => #no_field_panic, + } + } + + fn resolve_into_type_async<'b>( + &'b self, + info: &'b Self::TypeInfo, + type_name: &str, + _: Option<&'b [::juniper::Selection<'b, #scalar>]>, + executor: &'b ::juniper::Executor<'b, 'b, Self::Context, #scalar> + ) -> ::juniper::BoxFuture<'b, ::juniper::ExecutionResult<#scalar>> { + #( #custom_downcasts )* + #regular_downcast + } } } } } -/// Representation of custom downcast into an [`Implementer`] from a [GraphQL interface][1] type for -/// code generation. +/// Representation of custom downcast into an [`Implementer`] from a +/// [GraphQL interface][1] type for code generation. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces #[derive(Clone, Debug)] enum ImplementerDowncast { - /// Downcast is performed via a method of trait describing a [GraphQL interface][1]. + /// Downcast is performed via a method of trait describing a + /// [GraphQL interface][1]. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces Method { /// Name of trait method which performs this [`ImplementerDowncast`]. name: syn::Ident, - /// Indicator whether the trait method accepts a [`Context`] as its second argument. + /// Indicator whether the trait method accepts a [`Context`] as its + /// second argument. /// /// [`Context`]: juniper::Context with_context: bool, @@ -1399,30 +742,32 @@ enum ImplementerDowncast { /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces #[derive(Clone, Debug)] struct Implementer { - /// Rust type that this [GraphQL interface][1] [`Implementer`] is represented by. + /// Rust type that this [GraphQL interface][1] [`Implementer`] is + /// represented by. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces ty: syn::Type, /// Custom [`ImplementerDowncast`] for this [`Implementer`]. /// - /// If absent, then [`Implementer`] is downcast from an enum variant or a trait object. + /// If absent, then [`Implementer`] is downcast from an enum variant or a + /// trait object. downcast: Option, - /// Rust type of [`Context`] that this [GraphQL interface][1] [`Implementer`] requires for - /// downcasting. + /// Rust type of [`Context`] that this [GraphQL interface][1] + /// [`Implementer`] requires for downcasting. /// - /// It's available only when code generation happens for Rust traits and a trait method contains - /// context argument. + /// It's available only when code generation happens for Rust traits and a + /// trait method contains context argument. /// /// [`Context`]: juniper::Context /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces - context_ty: Option, + context: Option, /// [`ScalarValue`] parametrization of this [`Implementer`]. /// /// [`ScalarValue`]: juniper::ScalarValue - scalar: ScalarValueType, + scalar: scalar::Type, } impl Implementer { @@ -1456,8 +801,9 @@ impl Implementer { }) } - /// Returns generated code for the [`GraphQLValue::concrete_type_name`] method, which returns - /// name of the GraphQL type represented by this [`Implementer`]. + /// Returns generated code for the [`GraphQLValue::concrete_type_name`] + /// method, which returns name of the GraphQL type represented by this + /// [`Implementer`]. /// /// Returns [`None`] if there is no custom [`Implementer::downcast`]. /// @@ -1481,12 +827,13 @@ impl Implementer { }) } - /// Returns generated code for the [`GraphQLValue::resolve_into_type`] method, which downcasts - /// the [GraphQL interface][1] type into this [`Implementer`] synchronously. + /// Returns generated code for the [`GraphQLValue::resolve_into_type`][0] + /// method, which downcasts the [GraphQL interface][1] type into this + /// [`Implementer`] synchronously. /// /// Returns [`None`] if there is no custom [`Implementer::downcast`]. /// - /// [`GraphQLValue::resolve_into_type`]: juniper::GraphQLValue::resolve_into_type + /// [0]: juniper::GraphQLValue::resolve_into_type /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces #[must_use] fn method_resolve_into_type_tokens(&self, trait_ty: &syn::Type) -> Option { @@ -1507,8 +854,10 @@ impl Implementer { }) } - /// Returns generated code for the [`GraphQLValueAsync::resolve_into_type_async`][0] method, - /// which downcasts the [GraphQL interface][1] type into this [`Implementer`] asynchronously. + /// Returns generated code for the + /// [`GraphQLValueAsync::resolve_into_type_async`][0] method, which + /// downcasts the [GraphQL interface][1] type into this [`Implementer`] + /// asynchronously. /// /// Returns [`None`] if there is no custom [`Implementer::downcast`]. /// @@ -1534,7 +883,8 @@ impl Implementer { } } -/// Representation of Rust enum implementing [GraphQL interface][1] type for code generation. +/// Representation of Rust enum implementing [GraphQL interface][1] type for +/// code generation. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces struct EnumType { @@ -1544,54 +894,64 @@ struct EnumType { /// [`syn::Visibility`] of this [`EnumType`] to generate it with. visibility: syn::Visibility, - /// Rust types of all [GraphQL interface][1] implements to represent variants of this - /// [`EnumType`]. + /// Rust types of all [GraphQL interface][1] implements to represent + /// variants of this [`EnumType`]. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces variants: Vec, - /// Name of the trait describing the [GraphQL interface][1] represented by this [`EnumType`]. + /// Name of the trait describing the [GraphQL interface][1] represented by + /// this [`EnumType`]. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces trait_ident: syn::Ident, - /// [`syn::Generics`] of the trait describing the [GraphQL interface][1] represented by this - /// [`EnumType`]. + /// [`syn::Generics`] of the trait describing the [GraphQL interface][1] + /// represented by this [`EnumType`]. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces trait_generics: syn::Generics, - /// Associated types of the trait describing the [GraphQL interface][1] represented by this - /// [`EnumType`]. + /// Associated types of the trait describing the [GraphQL interface][1] + /// represented by this [`EnumType`]. trait_types: Vec<(syn::Ident, syn::Generics)>, - /// Associated constants of the trait describing the [GraphQL interface][1] represented by this - /// [`EnumType`]. + /// Associated constants of the trait describing the [GraphQL interface][1] + /// represented by this [`EnumType`]. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces trait_consts: Vec<(syn::Ident, syn::Type)>, - /// Methods of the trait describing the [GraphQL interface][1] represented by this [`EnumType`]. + /// Methods of the trait describing the [GraphQL interface][1] represented + /// by this [`EnumType`]. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces trait_methods: Vec, - /// [`ScalarValue`] parametrization to generate [`GraphQLType`] implementation with for this - /// [`EnumType`]. + /// [`ScalarValue`] parametrization to generate [`GraphQLType`] + /// implementation with for this [`EnumType`]. /// /// [`GraphQLType`]: juniper::GraphQLType /// [`ScalarValue`]: juniper::ScalarValue - scalar: ScalarValueType, + scalar: scalar::Type, +} + +impl ToTokens for EnumType { + fn to_tokens(&self, into: &mut TokenStream) { + self.type_definition_tokens().to_tokens(into); + into.append_all(self.impl_from_tokens()); + self.impl_trait_tokens().to_tokens(into); + } } impl EnumType { - /// Constructs new [`EnumType`] out of the given parameters. + /// Constructs a new [`EnumType`] out of the given parameters. #[must_use] fn new( r#trait: &syn::ItemTrait, - meta: &TraitMeta, + meta: &TraitAttr, implers: &[Implementer], - scalar: ScalarValueType, + scalar: scalar::Type, ) -> Self { Self { ident: meta @@ -1641,8 +1001,8 @@ impl EnumType { } } - /// Returns name of a single variant of this [`EnumType`] by the given underlying [`syn::Type`] - /// of the variant. + /// Returns name of a single variant of this [`EnumType`] by the given + /// underlying [`syn::Type`] of the variant. #[must_use] fn variant_ident(ty: &syn::Type) -> &syn::Ident { if let syn::Type::Path(p) = ty { @@ -1652,15 +1012,15 @@ impl EnumType { } } - /// Indicates whether this [`EnumType`] has non-exhaustive phantom variant to hold type - /// parameters. + /// Indicates whether this [`EnumType`] has non-exhaustive phantom variant + /// to hold type parameters. #[must_use] fn has_phantom_variant(&self) -> bool { !self.trait_generics.params.is_empty() } - /// Returns generate code for dispatching non-exhaustive phantom variant of this [`EnumType`] - /// in `match` expressions. + /// Returns generate code for dispatching non-exhaustive phantom variant of + /// this [`EnumType`] in `match` expressions. /// /// Returns [`None`] if this [`EnumType`] is exhaustive. #[must_use] @@ -1672,30 +1032,68 @@ impl EnumType { } } - /// Returns prepared [`syn::Generics`] for [`GraphQLType`] trait (and similar) implementation - /// for this [`EnumType`]. + /// Returns prepared [`syn::Generics`] for [`GraphQLType`] trait (and + /// similar) implementation of this [`EnumType`]. + /// + /// If `for_async` is `true`, then additional predicates are added to suit + /// the [`GraphQLAsyncValue`] trait (and similar) requirements. /// + /// [`GraphQLAsyncValue`]: juniper::GraphQLAsyncValue /// [`GraphQLType`]: juniper::GraphQLType #[must_use] - fn impl_generics(&self) -> syn::Generics { + fn impl_generics(&self, for_async: bool) -> syn::Generics { let mut generics = self.trait_generics.clone(); let scalar = &self.scalar; - if self.scalar.is_implicit_generic() { + if scalar.is_implicit_generic() { generics.params.push(parse_quote! { #scalar }); } - if self.scalar.is_generic() { + if scalar.is_generic() { generics .make_where_clause() .predicates .push(parse_quote! { #scalar: ::juniper::ScalarValue }); } + if let Some(bound) = scalar.bounds() { + generics.make_where_clause().predicates.push(bound); + } + + if for_async { + let self_ty = if self.trait_generics.lifetimes().next().is_some() { + // Modify lifetime names to omit "lifetime name `'a` shadows a + // lifetime name that is already in scope" error. + let mut generics = self.trait_generics.clone(); + for lt in generics.lifetimes_mut() { + let ident = lt.lifetime.ident.unraw(); + lt.lifetime.ident = format_ident!("__fa__{}", ident); + } + + let lifetimes = generics.lifetimes().map(|lt| <.lifetime); + let ty = &self.ident; + let (_, ty_generics, _) = generics.split_for_impl(); + + quote! { for<#( #lifetimes ),*> #ty#ty_generics } + } else { + quote! { Self } + }; + generics + .make_where_clause() + .predicates + .push(parse_quote! { #self_ty: Sync }); + + if scalar.is_generic() { + generics + .make_where_clause() + .predicates + .push(parse_quote! { #scalar: Send + Sync }); + } + } generics } - /// Returns full type signature of the original trait describing the [GraphQL interface][1] for - /// this [`EnumType`]. + /// Returns full type signature of the original trait describing the + /// [GraphQL interface][1] for this [`EnumType`]. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces #[must_use] @@ -1717,8 +1115,8 @@ impl EnumType { /// Returns generate code of the Rust type definitions of this [`EnumType`]. /// - /// If the [`EnumType::trait_generics`] are not empty, then they are contained in the generated - /// enum too. + /// If the [`EnumType::trait_generics`] are not empty, then they are + /// contained in the generated enum too. #[must_use] fn type_definition_tokens(&self) -> TokenStream { let enum_ty = &self.ident; @@ -1781,8 +1179,8 @@ impl EnumType { } } - /// Returns generated code implementing [`From`] trait for this [`EnumType`] from its - /// [`EnumType::variants`]. + /// Returns generated code implementing [`From`] trait for this [`EnumType`] + /// from its [`EnumType::variants`]. fn impl_from_tokens(&self) -> impl Iterator + '_ { let enum_ty = &self.ident; let (impl_generics, generics, where_clause) = self.trait_generics.split_for_impl(); @@ -1801,8 +1199,8 @@ impl EnumType { }) } - /// Returns generated code implementing the original trait describing the [GraphQL interface][1] - /// for this [`EnumType`]. + /// Returns generated code implementing the original trait describing the + /// [GraphQL interface][1] for this [`EnumType`]. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces #[must_use] @@ -1872,6 +1270,7 @@ impl EnumType { }); let mut impl_tokens = quote! { + #[allow(deprecated)] #[automatically_derived] impl#impl_generics #trait_ident#generics for #enum_ty#generics #where_clause { #( #assoc_types )* @@ -1901,10 +1300,11 @@ impl EnumType { impl_tokens } - /// Returns generated code for the [`GraphQLValue::concrete_type_name`] method, which returns - /// name of the underlying [`Implementer`] GraphQL type contained in this [`EnumType`]. + /// Returns generated code for the [`GraphQLValue::concrete_type_name`][0] + /// method, which returns name of the underlying [`Implementer`] GraphQL + /// type contained in this [`EnumType`]. /// - /// [`GraphQLValue::concrete_type_name`]: juniper::GraphQLValue::concrete_type_name + /// [0]: juniper::GraphQLValue::concrete_type_name #[must_use] fn method_concrete_type_name_tokens(&self) -> TokenStream { let scalar = &self.scalar; @@ -1928,10 +1328,11 @@ impl EnumType { } } - /// Returns generated code for the [`GraphQLValue::resolve_into_type`] method, which downcasts - /// this [`EnumType`] into its underlying [`Implementer`] type synchronously. + /// Returns generated code for the [`GraphQLValue::resolve_into_type`][0] + /// method, which downcasts this [`EnumType`] into its underlying + /// [`Implementer`] type synchronously. /// - /// [`GraphQLValue::resolve_into_type`]: juniper::GraphQLValue::resolve_into_type + /// [0]: juniper::GraphQLValue::resolve_into_type #[must_use] fn method_resolve_into_type_tokens(&self) -> TokenStream { let resolving_code = gen::sync_resolving_code(); @@ -1953,8 +1354,10 @@ impl EnumType { } } - /// Returns generated code for the [`GraphQLValueAsync::resolve_into_type_async`][0] method, - /// which downcasts this [`EnumType`] into its underlying [`Implementer`] type asynchronously. + /// Returns generated code for the + /// [`GraphQLValueAsync::resolve_into_type_async`][0] method, which + /// downcasts this [`EnumType`] into its underlying [`Implementer`] type + /// asynchronously. /// /// [0]: juniper::GraphQLValueAsync::resolve_into_type_async #[must_use] @@ -1982,16 +1385,8 @@ impl EnumType { } } -impl ToTokens for EnumType { - fn to_tokens(&self, into: &mut TokenStream) { - into.append_all(&[self.type_definition_tokens()]); - into.append_all(self.impl_from_tokens()); - into.append_all(&[self.impl_trait_tokens()]); - } -} - -/// Representation of Rust [trait object][2] implementing [GraphQL interface][1] type for code -/// generation. +/// Representation of Rust [trait object][2] implementing [GraphQL interface][1] +/// type for code generation. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces /// [2]: https://doc.rust-lang.org/reference/types/trait-object.html @@ -2002,39 +1397,38 @@ struct TraitObjectType { /// [`syn::Visibility`] of this [`TraitObjectType`] to generate it with. visibility: syn::Visibility, - /// Name of the trait describing the [GraphQL interface][1] represented by this - /// [`TraitObjectType`]. + /// Name of the trait describing the [GraphQL interface][1] represented by + /// this [`TraitObjectType`]. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces trait_ident: syn::Ident, - /// [`syn::Generics`] of the trait describing the [GraphQL interface][1] represented by this - /// [`TraitObjectType`]. + /// [`syn::Generics`] of the trait describing the [GraphQL interface][1] + /// represented by this [`TraitObjectType`]. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces trait_generics: syn::Generics, - /// [`ScalarValue`] parametrization of this [`TraitObjectType`] to generate it with. + /// [`ScalarValue`] parametrization of this [`TraitObjectType`] to generate + /// it with. /// /// [`ScalarValue`]: juniper::ScalarValue - scalar: ScalarValueType, + scalar: scalar::Type, /// Rust type of [`Context`] to generate this [`TraitObjectType`] with. /// - /// If [`None`] then generated code will use unit type `()` as [`Context`]. - /// /// [`Context`]: juniper::Context - context: Option, + context: syn::Type, } impl TraitObjectType { - /// Constructs new [`TraitObjectType`] out of the given parameters. + /// Constructs a new [`TraitObjectType`] out of the given parameters. #[must_use] fn new( r#trait: &syn::ItemTrait, - meta: &TraitMeta, - scalar: ScalarValueType, - context: Option, + meta: &TraitAttr, + scalar: scalar::Type, + context: syn::Type, ) -> Self { Self { ident: meta.r#dyn.as_ref().unwrap().as_ref().clone(), @@ -2046,12 +1440,16 @@ impl TraitObjectType { } } - /// Returns prepared [`syn::Generics`] for [`GraphQLType`] trait (and similar) implementation - /// for this [`TraitObjectType`]. + /// Returns prepared [`syn::Generics`] for [`GraphQLType`] trait (and + /// similar) implementation of this [`TraitObjectType`]. + /// + /// If `for_async` is `true`, then additional predicates are added to suit + /// the [`GraphQLAsyncValue`] trait (and similar) requirements. /// + /// [`GraphQLAsyncValue`]: juniper::GraphQLAsyncValue /// [`GraphQLType`]: juniper::GraphQLType #[must_use] - fn impl_generics(&self) -> syn::Generics { + fn impl_generics(&self, for_async: bool) -> syn::Generics { let mut generics = self.trait_generics.clone(); generics.params.push(parse_quote! { '__obj }); @@ -2066,12 +1464,28 @@ impl TraitObjectType { .predicates .push(parse_quote! { #scalar: ::juniper::ScalarValue }); } + if let Some(bound) = scalar.bounds() { + generics.make_where_clause().predicates.push(bound); + } + + if for_async { + generics + .make_where_clause() + .predicates + .push(parse_quote! { Self: Sync }); + if scalar.is_generic() { + generics + .make_where_clause() + .predicates + .push(parse_quote! { #scalar: Send + Sync }); + } + } generics } - /// Returns full type signature of the original trait describing the [GraphQL interface][1] for - /// this [`TraitObjectType`]. + /// Returns full type signature of the original trait describing the + /// [GraphQL interface][1] for this [`TraitObjectType`]. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces #[must_use] @@ -2088,7 +1502,8 @@ impl TraitObjectType { parse_quote! { #ty#generics } } - /// Returns generated code of the full type signature of this [`TraitObjectType`]. + /// Returns generated code of the full type signature of this + /// [`TraitObjectType`]. #[must_use] fn ty_tokens(&self) -> TokenStream { let ty = &self.trait_ident; @@ -2102,17 +1517,18 @@ impl TraitObjectType { } let ty_params = &generics.params; - let context = self.context.clone().unwrap_or_else(|| parse_quote! { () }); + let context = &self.context; quote! { dyn #ty<#ty_params, Context = #context, TypeInfo = ()> + '__obj + Send + Sync } } - /// Returns generated code for the [`GraphQLValue::concrete_type_name`] method, which returns - /// name of the underlying [`Implementer`] GraphQL type contained in this [`TraitObjectType`]. + /// Returns generated code for the [`GraphQLValue::concrete_type_name`][0] + /// method, which returns name of the underlying [`Implementer`] GraphQL + /// type contained in this [`TraitObjectType`]. /// - /// [`GraphQLValue::concrete_type_name`]: juniper::GraphQLValue::concrete_type_name + /// [0]: juniper::GraphQLValue::concrete_type_name #[must_use] fn method_concrete_type_name_tokens(&self) -> TokenStream { quote! { @@ -2120,10 +1536,11 @@ impl TraitObjectType { } } - /// Returns generated code for the [`GraphQLValue::resolve_into_type`] method, which downcasts - /// this [`TraitObjectType`] into its underlying [`Implementer`] type synchronously. + /// Returns generated code for the [`GraphQLValue::resolve_into_type`][0] + /// method, which downcasts this [`TraitObjectType`] into its underlying + /// [`Implementer`] type synchronously. /// - /// [`GraphQLValue::resolve_into_type`]: juniper::GraphQLValue::resolve_into_type + /// [0]: juniper::GraphQLValue::resolve_into_type #[must_use] fn method_resolve_into_type_tokens(&self) -> TokenStream { let resolving_code = gen::sync_resolving_code(); @@ -2134,9 +1551,10 @@ impl TraitObjectType { } } - /// Returns generated code for the [`GraphQLValueAsync::resolve_into_type_async`][0] method, - /// which downcasts this [`TraitObjectType`] into its underlying [`Implementer`] type - /// asynchronously. + /// Returns generated code for the + /// [`GraphQLValueAsync::resolve_into_type_async`][0] method, which + /// downcasts this [`TraitObjectType`] into its underlying [`Implementer`] + /// type asynchronously. /// /// [0]: juniper::GraphQLValueAsync::resolve_into_type_async #[must_use] @@ -2186,7 +1604,7 @@ impl ToTokens for TraitObjectType { ty_params_right = Some(quote! { #params, }); }; - let context = self.context.clone().unwrap_or_else(|| parse_quote! { () }); + let context = &self.context; let dyn_alias = quote! { #[automatically_derived] @@ -2200,8 +1618,8 @@ impl ToTokens for TraitObjectType { } } -/// Representation of possible Rust types implementing [GraphQL interface][1] type for code -/// generation. +/// Representation of possible Rust types implementing [GraphQL interface][1] +/// type for code generation. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces enum Type { @@ -2217,21 +1635,36 @@ enum Type { TraitObject(Box), } +impl ToTokens for Type { + fn to_tokens(&self, into: &mut TokenStream) { + match self { + Self::Enum(e) => e.to_tokens(into), + Self::TraitObject(o) => o.to_tokens(into), + } + } +} + impl Type { - /// Returns prepared [`syn::Generics`] for [`GraphQLType`] trait (and similar) implementation - /// for this [`Type`]. + /// Returns prepared [`syn::Generics`] for [`GraphQLType`] trait (and + /// similar) implementation of this [`Type`]. /// + /// If `for_async` is `true`, then additional predicates are added to suit + /// the [`GraphQLAsyncValue`] trait (and similar) requirements. + /// + /// [`GraphQLAsyncValue`]: juniper::GraphQLAsyncValue /// [`GraphQLType`]: juniper::GraphQLType #[must_use] - fn impl_generics(&self) -> syn::Generics { - match self { - Self::Enum(e) => e.impl_generics(), - Self::TraitObject(o) => o.impl_generics(), - } + fn impl_generics(&self, for_async: bool) -> (TokenStream, Option) { + let generics = match self { + Self::Enum(e) => e.impl_generics(for_async), + Self::TraitObject(o) => o.impl_generics(for_async), + }; + let (impl_generics, _, where_clause) = generics.split_for_impl(); + (quote! { #impl_generics }, where_clause.cloned()) } - /// Returns full type signature of the original trait describing the [GraphQL interface][1] for - /// this [`Type`]. + /// Returns full type signature of the original trait describing the + /// [GraphQL interface][1] for this [`Type`]. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces #[must_use] @@ -2251,10 +1684,11 @@ impl Type { } } - /// Returns generated code for the [`GraphQLValue::concrete_type_name`] method, which returns - /// name of the underlying [`Implementer`] GraphQL type contained in this [`Type`]. + /// Returns generated code for the [`GraphQLValue::concrete_type_name`][0] + /// method, which returns name of the underlying [`Implementer`] GraphQL + /// type contained in this [`Type`]. /// - /// [`GraphQLValue::concrete_type_name`]: juniper::GraphQLValue::concrete_type_name + /// [0]: juniper::GraphQLValue::concrete_type_name #[must_use] fn method_concrete_type_name_tokens(&self) -> TokenStream { match self { @@ -2263,10 +1697,11 @@ impl Type { } } - /// Returns generated code for the [`GraphQLValue::resolve_into_type`] method, which downcasts - /// this [`Type`] into its underlying [`Implementer`] type synchronously. + /// Returns generated code for the [`GraphQLValue::resolve_into_type`][0] + /// method, which downcasts this [`Type`] into its underlying + /// [`Implementer`] type synchronously. /// - /// [`GraphQLValue::resolve_into_type`]: juniper::GraphQLValue::resolve_into_type + /// [0]: juniper::GraphQLValue::resolve_into_type #[must_use] fn method_resolve_into_type_tokens(&self) -> TokenStream { match self { @@ -2275,8 +1710,10 @@ impl Type { } } - /// Returns generated code for the [`GraphQLValueAsync::resolve_into_type_async`][0] method, - /// which downcasts this [`Type`] into its underlying [`Implementer`] type asynchronously. + /// Returns generated code for the + /// [`GraphQLValueAsync::resolve_into_type_async`][0] method, which + /// downcasts this [`Type`] into its underlying [`Implementer`] type + /// asynchronously. /// /// [0]: juniper::GraphQLValueAsync::resolve_into_type_async fn method_resolve_into_type_async_tokens(&self) -> TokenStream { @@ -2287,18 +1724,9 @@ impl Type { } } -impl ToTokens for Type { - fn to_tokens(&self, into: &mut TokenStream) { - match self { - Self::Enum(e) => e.to_tokens(into), - Self::TraitObject(o) => o.to_tokens(into), - } - } -} - -/// Injects [`async_trait`] implementation into the given trait definition or trait implementation -/// block, correctly restricting type and lifetime parameters with `'async_trait` lifetime, if -/// required. +/// Injects [`async_trait`] implementation into the given trait definition or +/// trait implementation block, correctly restricting type and lifetime +/// parameters with `'async_trait` lifetime, if required. fn inject_async_trait<'m, M>(attrs: &mut Vec, methods: M, generics: &syn::Generics) where M: IntoIterator, diff --git a/juniper_codegen/src/graphql_object/attr.rs b/juniper_codegen/src/graphql_object/attr.rs new file mode 100644 index 000000000..34a70c211 --- /dev/null +++ b/juniper_codegen/src/graphql_object/attr.rs @@ -0,0 +1,255 @@ +//! Code generation for `#[graphql_object]` macro. + +use std::{any::TypeId, marker::PhantomData, mem}; + +use proc_macro2::{Span, TokenStream}; +use quote::{quote, ToTokens}; +use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned}; + +use crate::{ + common::{ + field, + parse::{self, TypeExt as _}, + scalar, + }, + result::GraphQLScope, + util::{path_eq_single, span_container::SpanContainer, RenameRule}, +}; + +use super::{Attr, Definition, Query}; + +/// [`GraphQLScope`] of errors for `#[graphql_object]` macro. +const ERR: GraphQLScope = GraphQLScope::ObjectAttr; + +/// Expands `#[graphql_object]` macro into generated code. +pub fn expand(attr_args: TokenStream, body: TokenStream) -> syn::Result { + if let Ok(mut ast) = syn::parse2::(body) { + if ast.trait_.is_none() { + let impl_attrs = parse::attr::unite(("graphql_object", &attr_args), &ast.attrs); + ast.attrs = parse::attr::strip("graphql_object", ast.attrs); + return expand_on_impl::(Attr::from_attrs("graphql_object", &impl_attrs)?, ast); + } + } + + Err(syn::Error::new( + Span::call_site(), + "#[graphql_object] attribute is applicable to non-trait `impl` blocks only", + )) +} + +/// Expands `#[graphql_object]` macro placed on an implementation block. +pub(crate) fn expand_on_impl( + attr: Attr, + mut ast: syn::ItemImpl, +) -> syn::Result +where + Definition: ToTokens, + Operation: 'static, +{ + let type_span = ast.self_ty.span(); + let type_ident = ast.self_ty.topmost_ident().ok_or_else(|| { + ERR.custom_error(type_span, "could not determine ident for the `impl` type") + })?; + + let name = attr + .name + .clone() + .map(SpanContainer::into_inner) + .unwrap_or_else(|| type_ident.unraw().to_string()); + if !attr.is_internal && name.starts_with("__") { + ERR.no_double_underscore( + attr.name + .as_ref() + .map(SpanContainer::span_ident) + .unwrap_or_else(|| type_ident.span()), + ); + } + + let scalar = scalar::Type::parse(attr.scalar.as_deref(), &ast.generics); + + proc_macro_error::abort_if_dirty(); + + let renaming = attr + .rename_fields + .as_deref() + .copied() + .unwrap_or(RenameRule::CamelCase); + + let async_only = TypeId::of::() != TypeId::of::(); + let fields: Vec<_> = ast + .items + .iter_mut() + .filter_map(|item| { + if let syn::ImplItem::Method(m) = item { + parse_field(m, async_only, &renaming) + } else { + None + } + }) + .collect(); + + proc_macro_error::abort_if_dirty(); + + if fields.is_empty() { + ERR.emit_custom(type_span, "must have at least one field"); + } + if !field::all_different(&fields) { + ERR.emit_custom(type_span, "must have a different name for each field"); + } + + proc_macro_error::abort_if_dirty(); + + let context = attr + .context + .as_deref() + .cloned() + .or_else(|| { + fields.iter().find_map(|f| { + f.arguments.as_ref().and_then(|f| { + f.iter() + .find_map(field::MethodArgument::context_ty) + .cloned() + }) + }) + }) + .unwrap_or_else(|| parse_quote! { () }); + + let generated_code = Definition:: { + name, + ty: ast.self_ty.unparenthesized().clone(), + generics: ast.generics.clone(), + description: attr.description.map(SpanContainer::into_inner), + context, + scalar, + fields, + interfaces: attr + .interfaces + .iter() + .map(|ty| ty.as_ref().clone()) + .collect(), + _operation: PhantomData, + }; + + Ok(quote! { + #ast + #generated_code + }) +} + +/// Parses a [`field::Definition`] from the given Rust [`syn::ImplItemMethod`]. +/// +/// Returns [`None`] if parsing fails, or the method field is ignored. +#[must_use] +fn parse_field( + method: &mut syn::ImplItemMethod, + async_only: bool, + renaming: &RenameRule, +) -> Option { + let method_attrs = method.attrs.clone(); + + // Remove repeated attributes from the method, to omit incorrect expansion. + method.attrs = mem::take(&mut method.attrs) + .into_iter() + .filter(|attr| !path_eq_single(&attr.path, "graphql")) + .collect(); + + let attr = field::Attr::from_attrs("graphql", &method_attrs) + .map_err(|e| proc_macro_error::emit_error!(e)) + .ok()?; + + if attr.ignore.is_some() { + return None; + } + + if async_only && method.sig.asyncness.is_none() { + return err_no_sync_resolvers(&method.sig); + } + + let method_ident = &method.sig.ident; + + let name = attr + .name + .as_ref() + .map(|m| m.as_ref().value()) + .unwrap_or_else(|| renaming.apply(&method_ident.unraw().to_string())); + if name.starts_with("__") { + ERR.no_double_underscore( + attr.name + .as_ref() + .map(SpanContainer::span_ident) + .unwrap_or_else(|| method_ident.span()), + ); + return None; + } + + let arguments = { + if let Some(arg) = method.sig.inputs.first() { + match arg { + syn::FnArg::Receiver(rcv) => { + if rcv.reference.is_none() || rcv.mutability.is_some() { + return err_invalid_method_receiver(rcv); + } + } + syn::FnArg::Typed(arg) => { + if let syn::Pat::Ident(a) = &*arg.pat { + if a.ident.to_string().as_str() == "self" { + return err_invalid_method_receiver(arg); + } + } + } + } + } + method + .sig + .inputs + .iter_mut() + .filter_map(|arg| match arg { + syn::FnArg::Receiver(_) => None, + syn::FnArg::Typed(arg) => field::MethodArgument::parse(arg, renaming, &ERR), + }) + .collect() + }; + + let mut ty = match &method.sig.output { + syn::ReturnType::Default => parse_quote! { () }, + syn::ReturnType::Type(_, ty) => ty.unparenthesized().clone(), + }; + ty.lifetimes_anonymized(); + + let description = attr.description.as_ref().map(|d| d.as_ref().value()); + let deprecated = attr + .deprecated + .as_deref() + .map(|d| d.as_ref().map(syn::LitStr::value)); + + Some(field::Definition { + name, + ty, + description, + deprecated, + ident: method_ident.clone(), + arguments: Some(arguments), + has_receiver: method.sig.receiver().is_some(), + is_async: method.sig.asyncness.is_some(), + }) +} + +/// Emits "invalid method receiver" [`syn::Error`] pointing to the given `span`. +#[must_use] +fn err_invalid_method_receiver(span: &S) -> Option { + ERR.emit_custom( + span.span(), + "method should have a shared reference receiver `&self`, or no receiver at all", + ); + None +} + +/// Emits "synchronous resolvers are not supported" [`syn::Error`] pointing to +/// the given `span`. +#[must_use] +fn err_no_sync_resolvers(span: &S) -> Option { + ERR.custom(span.span(), "synchronous resolvers are not supported") + .note("Specify that this function is async: `async fn foo()`".into()) + .emit(); + None +} diff --git a/juniper_codegen/src/graphql_object/derive.rs b/juniper_codegen/src/graphql_object/derive.rs new file mode 100644 index 000000000..7cbe961b8 --- /dev/null +++ b/juniper_codegen/src/graphql_object/derive.rs @@ -0,0 +1,160 @@ +//! Code generation for `#[derive(GraphQLObject)]` macro. + +use std::marker::PhantomData; + +use proc_macro2::TokenStream; +use proc_macro_error::ResultExt as _; +use quote::ToTokens; +use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned as _}; + +use crate::{ + common::{field, parse::TypeExt as _, scalar}, + result::GraphQLScope, + util::{span_container::SpanContainer, RenameRule}, +}; + +use super::{Attr, Definition, Query}; + +/// [`GraphQLScope`] of errors for `#[derive(GraphQLObject)]` macro. +const ERR: GraphQLScope = GraphQLScope::ObjectDerive; + +/// Expands `#[derive(GraphQLObject)]` macro into generated code. +pub fn expand(input: TokenStream) -> syn::Result { + let ast = syn::parse2::(input).unwrap_or_abort(); + + match &ast.data { + syn::Data::Struct(_) => expand_struct(ast), + _ => Err(ERR.custom_error(ast.span(), "can only be derived for structs")), + } + .map(ToTokens::into_token_stream) +} + +/// Expands into generated code a `#[derive(GraphQLObject)]` macro placed on a +/// Rust struct. +fn expand_struct(ast: syn::DeriveInput) -> syn::Result> { + let attr = Attr::from_attrs("graphql", &ast.attrs)?; + + let struct_span = ast.span(); + let struct_ident = ast.ident; + + let (_, struct_generics, _) = ast.generics.split_for_impl(); + let ty = parse_quote! { #struct_ident#struct_generics }; + + let name = attr + .name + .clone() + .map(SpanContainer::into_inner) + .unwrap_or_else(|| struct_ident.unraw().to_string()); + if !attr.is_internal && name.starts_with("__") { + ERR.no_double_underscore( + attr.name + .as_ref() + .map(SpanContainer::span_ident) + .unwrap_or_else(|| struct_ident.span()), + ); + } + + let scalar = scalar::Type::parse(attr.scalar.as_deref(), &ast.generics); + + proc_macro_error::abort_if_dirty(); + + let renaming = attr + .rename_fields + .as_deref() + .copied() + .unwrap_or(RenameRule::CamelCase); + + let mut fields = vec![]; + if let syn::Data::Struct(data) = &ast.data { + if let syn::Fields::Named(fs) = &data.fields { + fields = fs + .named + .iter() + .filter_map(|f| parse_field(f, &renaming)) + .collect(); + } else { + ERR.emit_custom(struct_span, "only named fields are allowed"); + } + } + + proc_macro_error::abort_if_dirty(); + + if fields.is_empty() { + ERR.emit_custom(struct_span, "must have at least one field"); + } + if !field::all_different(&fields) { + ERR.emit_custom(struct_span, "must have a different name for each field"); + } + + proc_macro_error::abort_if_dirty(); + + Ok(Definition { + name, + ty, + generics: ast.generics, + description: attr.description.map(SpanContainer::into_inner), + context: attr + .context + .map(SpanContainer::into_inner) + .unwrap_or_else(|| parse_quote! { () }), + scalar, + fields, + interfaces: attr + .interfaces + .iter() + .map(|ty| ty.as_ref().clone()) + .collect(), + _operation: PhantomData, + }) +} + +/// Parses a [`field::Definition`] from the given Rust struct [`syn::Field`]. +/// +/// Returns [`None`] if parsing fails, or the struct field is ignored. +#[must_use] +fn parse_field(field: &syn::Field, renaming: &RenameRule) -> Option { + let attr = field::Attr::from_attrs("graphql", &field.attrs) + .map_err(|e| proc_macro_error::emit_error!(e)) + .ok()?; + + if attr.ignore.is_some() { + return None; + } + + let field_ident = field.ident.as_ref().unwrap(); + + let name = attr + .name + .as_ref() + .map(|m| m.as_ref().value()) + .unwrap_or_else(|| renaming.apply(&field_ident.unraw().to_string())); + if name.starts_with("__") { + ERR.no_double_underscore( + attr.name + .as_ref() + .map(SpanContainer::span_ident) + .unwrap_or_else(|| field_ident.span()), + ); + return None; + } + + let mut ty = field.ty.unparenthesized().clone(); + ty.lifetimes_anonymized(); + + let description = attr.description.as_ref().map(|d| d.as_ref().value()); + let deprecated = attr + .deprecated + .as_deref() + .map(|d| d.as_ref().map(syn::LitStr::value)); + + Some(field::Definition { + name, + ty, + description, + deprecated, + ident: field_ident.clone(), + arguments: None, + has_receiver: false, + is_async: false, + }) +} diff --git a/juniper_codegen/src/graphql_object/mod.rs b/juniper_codegen/src/graphql_object/mod.rs new file mode 100644 index 000000000..267fde795 --- /dev/null +++ b/juniper_codegen/src/graphql_object/mod.rs @@ -0,0 +1,622 @@ +//! Code generation for [GraphQL object][1]. +//! +//! [1]: https://spec.graphql.org/June2018/#sec-Objects + +pub mod attr; +pub mod derive; + +use std::{any::TypeId, collections::HashSet, convert::TryInto as _, marker::PhantomData}; + +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens}; +use syn::{ + parse::{Parse, ParseStream}, + parse_quote, + spanned::Spanned as _, + token, +}; + +use crate::{ + common::{ + field, + parse::{ + attr::{err, OptionExt as _}, + ParseBufferExt as _, TypeExt, + }, + scalar, + }, + util::{filter_attrs, get_doc_comment, span_container::SpanContainer, RenameRule}, +}; +use syn::ext::IdentExt; + +/// Available arguments behind `#[graphql]` (or `#[graphql_object]`) attribute +/// when generating code for [GraphQL object][1] type. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Objects +#[derive(Debug, Default)] +pub(crate) struct Attr { + /// Explicitly specified name of this [GraphQL object][1] type. + /// + /// If [`None`], then Rust type name is used by default. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + pub(crate) name: Option>, + + /// Explicitly specified [description][2] of this [GraphQL object][1] type. + /// + /// If [`None`], then Rust doc comment is used as [description][2], if any. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + /// [2]: https://spec.graphql.org/June2018/#sec-Descriptions + pub(crate) description: Option>, + + /// Explicitly specified type of [`Context`] to use for resolving this + /// [GraphQL object][1] type with. + /// + /// If [`None`], then unit type `()` is assumed as a type of [`Context`]. + /// + /// [`Context`]: juniper::Context + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + pub(crate) context: Option>, + + /// Explicitly specified type (or type parameter with its bounds) of + /// [`ScalarValue`] to use for resolving this [GraphQL object][1] type with. + /// + /// If [`None`], then generated code will be generic over any + /// [`ScalarValue`] type, which, in turn, requires all [object][1] fields to + /// be generic over any [`ScalarValue`] type too. That's why this type + /// should be specified only if one of the variants implements + /// [`GraphQLType`] in a non-generic way over [`ScalarValue`] type. + /// + /// [`GraphQLType`]: juniper::GraphQLType + /// [`ScalarValue`]: juniper::ScalarValue + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + pub(crate) scalar: Option>, + + /// Explicitly specified [GraphQL interfaces][2] this [GraphQL object][1] + /// type implements. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + /// [2]: https://spec.graphql.org/June2018/#sec-Interfaces + pub(crate) interfaces: HashSet>, + + /// Explicitly specified [`RenameRule`] for all fields of this + /// [GraphQL object][1] type. + /// + /// If [`None`] then the default rule will be [`RenameRule::CamelCase`]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + pub(crate) rename_fields: Option>, + + /// Indicator whether the generated code is intended to be used only inside + /// the [`juniper`] library. + pub(crate) is_internal: bool, +} + +impl Parse for Attr { + fn parse(input: ParseStream<'_>) -> syn::Result { + let mut out = Self::default(); + while !input.is_empty() { + let ident = input.parse_any_ident()?; + match ident.to_string().as_str() { + "name" => { + input.parse::()?; + let name = input.parse::()?; + out.name + .replace(SpanContainer::new( + ident.span(), + Some(name.span()), + name.value(), + )) + .none_or_else(|_| err::dup_arg(&ident))? + } + "desc" | "description" => { + input.parse::()?; + let desc = input.parse::()?; + out.description + .replace(SpanContainer::new( + ident.span(), + Some(desc.span()), + desc.value(), + )) + .none_or_else(|_| err::dup_arg(&ident))? + } + "ctx" | "context" | "Context" => { + input.parse::()?; + let ctx = input.parse::()?; + out.context + .replace(SpanContainer::new(ident.span(), Some(ctx.span()), ctx)) + .none_or_else(|_| err::dup_arg(&ident))? + } + "scalar" | "Scalar" | "ScalarValue" => { + input.parse::()?; + let scl = input.parse::()?; + out.scalar + .replace(SpanContainer::new(ident.span(), Some(scl.span()), scl)) + .none_or_else(|_| err::dup_arg(&ident))? + } + "impl" | "implements" | "interfaces" => { + input.parse::()?; + for iface in input.parse_maybe_wrapped_and_punctuated::< + syn::Type, token::Bracket, token::Comma, + >()? { + let iface_span = iface.span(); + out + .interfaces + .replace(SpanContainer::new(ident.span(), Some(iface_span), iface)) + .none_or_else(|_| err::dup_arg(iface_span))?; + } + } + "rename_all" => { + input.parse::()?; + let val = input.parse::()?; + out.rename_fields + .replace(SpanContainer::new( + ident.span(), + Some(val.span()), + val.try_into()?, + )) + .none_or_else(|_| err::dup_arg(&ident))?; + } + "internal" => { + out.is_internal = true; + } + name => { + return Err(err::unknown_arg(&ident, name)); + } + } + input.try_parse::()?; + } + Ok(out) + } +} + +impl Attr { + /// Tries to merge two [`Attr`]s into a single one, reporting about + /// duplicates, if any. + fn try_merge(self, mut another: Self) -> syn::Result { + Ok(Self { + name: try_merge_opt!(name: self, another), + description: try_merge_opt!(description: self, another), + context: try_merge_opt!(context: self, another), + scalar: try_merge_opt!(scalar: self, another), + interfaces: try_merge_hashset!(interfaces: self, another => span_joined), + rename_fields: try_merge_opt!(rename_fields: self, another), + is_internal: self.is_internal || another.is_internal, + }) + } + + /// Parses [`Attr`] from the given multiple `name`d [`syn::Attribute`]s + /// placed on a struct or impl block definition. + pub(crate) fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result { + let mut attr = filter_attrs(name, attrs) + .map(|attr| attr.parse_args()) + .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?; + + if attr.description.is_none() { + attr.description = get_doc_comment(attrs); + } + + Ok(attr) + } +} + +/// Definition of [GraphQL object][1] for code generation. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Objects +#[derive(Debug)] +pub(crate) struct Definition { + /// Name of this [GraphQL object][1] in GraphQL schema. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + pub(crate) name: String, + + /// Rust type that this [GraphQL object][1] is represented with. + /// + /// It should contain all its generics, if any. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + pub(crate) ty: syn::Type, + + /// Generics of the Rust type that this [GraphQL object][1] is implemented + /// for. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + pub(crate) generics: syn::Generics, + + /// Description of this [GraphQL object][1] to put into GraphQL schema. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + pub(crate) description: Option, + + /// Rust type of [`Context`] to generate [`GraphQLType`] implementation with + /// for this [GraphQL object][1]. + /// + /// [`GraphQLType`]: juniper::GraphQLType + /// [`Context`]: juniper::Context + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + pub(crate) context: syn::Type, + + /// [`ScalarValue`] parametrization to generate [`GraphQLType`] + /// implementation with for this [GraphQL object][1]. + /// + /// [`GraphQLType`]: juniper::GraphQLType + /// [`ScalarValue`]: juniper::ScalarValue + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + pub(crate) scalar: scalar::Type, + + /// Defined [GraphQL fields][2] of this [GraphQL object][1]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + /// [2]: https://spec.graphql.org/June2018/#sec-Language.Fields + pub(crate) fields: Vec, + + /// [GraphQL interfaces][2] implemented by this [GraphQL object][1]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + /// [2]: https://spec.graphql.org/June2018/#sec-Interfaces + pub(crate) interfaces: HashSet, + + /// [GraphQL operation][1] this [`Definition`] should generate code for. + /// + /// Either [GraphQL query][2] or [GraphQL subscription][3]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Language.Operations + /// [2]: https://spec.graphql.org/June2018/#sec-Query + /// [3]: https://spec.graphql.org/June2018/#sec-Subscription + pub(crate) _operation: PhantomData>, +} + +impl Definition { + /// Returns prepared [`syn::Generics::split_for_impl`] for [`GraphQLType`] + /// trait (and similar) implementation of this [GraphQL object][1]. + /// + /// If `for_async` is `true`, then additional predicates are added to suit + /// the [`GraphQLAsyncValue`] trait (and similar) requirements. + /// + /// [`GraphQLAsyncValue`]: juniper::GraphQLAsyncValue + /// [`GraphQLType`]: juniper::GraphQLType + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + #[must_use] + pub(crate) fn impl_generics(&self, for_async: bool) -> (TokenStream, Option) { + let mut generics = self.generics.clone(); + + let scalar = &self.scalar; + if scalar.is_implicit_generic() { + generics.params.push(parse_quote! { #scalar }); + } + if scalar.is_generic() { + generics + .make_where_clause() + .predicates + .push(parse_quote! { #scalar: ::juniper::ScalarValue }); + } + if let Some(bound) = scalar.bounds() { + generics.make_where_clause().predicates.push(bound); + } + + if for_async { + let self_ty = if self.generics.lifetimes().next().is_some() { + let mut lifetimes = vec![]; + + // Modify lifetime names to omit "lifetime name `'a` shadows a + // lifetime name that is already in scope" error. + let mut ty = self.ty.clone(); + ty.lifetimes_iter_mut(&mut |lt| { + let ident = lt.ident.unraw(); + lt.ident = format_ident!("__fa__{}", ident); + lifetimes.push(lt.clone()); + }); + + quote! { for<#( #lifetimes ),*> #ty } + } else { + quote! { Self } + }; + generics + .make_where_clause() + .predicates + .push(parse_quote! { #self_ty: Sync }); + + if scalar.is_generic() { + generics + .make_where_clause() + .predicates + .push(parse_quote! { #scalar: Send + Sync }); + } + } + + let (impl_generics, _, where_clause) = generics.split_for_impl(); + (quote! { #impl_generics }, where_clause.cloned()) + } + + /// Returns generated code implementing [`marker::IsOutputType`] trait for + /// this [GraphQL object][1]. + /// + /// [`marker::IsOutputType`]: juniper::marker::IsOutputType + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + #[must_use] + pub(crate) fn impl_output_type_tokens(&self) -> TokenStream { + let scalar = &self.scalar; + + let (impl_generics, where_clause) = self.impl_generics(false); + let ty = &self.ty; + + let coerce_result = TypeId::of::() != TypeId::of::(); + let fields_marks = self + .fields + .iter() + .map(|f| f.method_mark_tokens(coerce_result, scalar)); + + let interface_tys = self.interfaces.iter(); + + quote! { + #[automatically_derived] + impl#impl_generics ::juniper::marker::IsOutputType<#scalar> for #ty #where_clause + { + fn mark() { + #( #fields_marks )* + #( <#interface_tys as ::juniper::marker::IsOutputType<#scalar>>::mark(); )* + } + } + } + } + + /// Returns generated code implementing [`GraphQLType`] trait for this + /// [GraphQL object][1]. + /// + /// [`GraphQLType`]: juniper::GraphQLType + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + #[must_use] + pub(crate) fn impl_graphql_type_tokens(&self) -> TokenStream { + let scalar = &self.scalar; + + let (impl_generics, where_clause) = self.impl_generics(false); + let ty = &self.ty; + + let name = &self.name; + let description = self + .description + .as_ref() + .map(|desc| quote! { .description(#desc) }); + + let extract_stream_type = TypeId::of::() != TypeId::of::(); + let fields_meta = self + .fields + .iter() + .map(|f| f.method_meta_tokens(extract_stream_type.then(|| scalar))); + + // Sorting is required to preserve/guarantee the order of interfaces registered in schema. + let mut interface_tys: Vec<_> = self.interfaces.iter().collect(); + interface_tys.sort_unstable_by(|a, b| { + let (a, b) = (quote!(#a).to_string(), quote!(#b).to_string()); + a.cmp(&b) + }); + let interfaces = (!interface_tys.is_empty()).then(|| { + quote! { + .interfaces(&[ + #( registry.get_type::<#interface_tys>(info), )* + ]) + } + }); + + quote! { + #[automatically_derived] + impl#impl_generics ::juniper::GraphQLType<#scalar> for #ty #where_clause + { + fn name(_ : &Self::TypeInfo) -> Option<&'static str> { + Some(#name) + } + + fn meta<'r>( + info: &Self::TypeInfo, + registry: &mut ::juniper::Registry<'r, #scalar> + ) -> ::juniper::meta::MetaType<'r, #scalar> + where #scalar: 'r, + { + let fields = [ + #( #fields_meta, )* + ]; + registry.build_object_type::<#ty>(info, &fields) + #description + #interfaces + .into_meta() + } + } + } + } +} + +/// [GraphQL query operation][2] of the [`Definition`] to generate code for. +/// +/// [2]: https://spec.graphql.org/June2018/#sec-Query +struct Query; + +impl ToTokens for Definition { + fn to_tokens(&self, into: &mut TokenStream) { + self.impl_graphql_object_tokens().to_tokens(into); + self.impl_output_type_tokens().to_tokens(into); + self.impl_graphql_type_tokens().to_tokens(into); + self.impl_graphql_value_tokens().to_tokens(into); + self.impl_graphql_value_async_tokens().to_tokens(into); + self.impl_as_dyn_graphql_value_tokens().to_tokens(into); + } +} + +impl Definition { + /// Returns generated code implementing [`GraphQLObject`] trait for this + /// [GraphQL object][1]. + /// + /// [`GraphQLObject`]: juniper::GraphQLObject + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + #[must_use] + fn impl_graphql_object_tokens(&self) -> TokenStream { + let scalar = &self.scalar; + + let (impl_generics, where_clause) = self.impl_generics(false); + let ty = &self.ty; + + let interface_tys = self.interfaces.iter(); + // TODO: Make it work by repeating `sa::assert_type_ne_all!` expansion, + // but considering generics. + //let interface_tys: Vec<_> = self.interfaces.iter().collect(); + //let all_interfaces_unique = (interface_tys.len() > 1).then(|| { + // quote! { ::juniper::sa::assert_type_ne_all!(#( #interface_tys ),*); } + //}); + + quote! { + #[automatically_derived] + impl#impl_generics ::juniper::marker::GraphQLObject<#scalar> for #ty #where_clause + { + fn mark() { + #( <#interface_tys as ::juniper::marker::GraphQLInterface<#scalar>>::mark(); )* + } + } + } + } + + /// Returns generated code implementing [`GraphQLValue`] trait for this + /// [GraphQL object][1]. + /// + /// [`GraphQLValue`]: juniper::GraphQLValue + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + #[must_use] + fn impl_graphql_value_tokens(&self) -> TokenStream { + let scalar = &self.scalar; + let context = &self.context; + + let (impl_generics, where_clause) = self.impl_generics(false); + let ty = &self.ty; + + let name = &self.name; + + let fields_resolvers = self + .fields + .iter() + .filter_map(|f| f.method_resolve_field_tokens(scalar, None)); + let async_fields_panic = { + let names = self + .fields + .iter() + .filter_map(|f| f.is_async.then(|| f.name.as_str())) + .collect::>(); + (!names.is_empty()).then(|| { + field::Definition::method_resolve_field_panic_async_field_tokens(&names, scalar) + }) + }; + let no_field_panic = field::Definition::method_resolve_field_panic_no_field_tokens(scalar); + + quote! { + #[allow(deprecated)] + #[automatically_derived] + impl#impl_generics ::juniper::GraphQLValue<#scalar> for #ty #where_clause + { + type Context = #context; + type TypeInfo = (); + + fn type_name<'__i>(&self, info: &'__i Self::TypeInfo) -> Option<&'__i str> { + >::name(info) + } + + fn resolve_field( + &self, + info: &Self::TypeInfo, + field: &str, + args: &::juniper::Arguments<#scalar>, + executor: &::juniper::Executor, + ) -> ::juniper::ExecutionResult<#scalar> { + match field { + #( #fields_resolvers )* + #async_fields_panic + _ => #no_field_panic, + } + } + + fn concrete_type_name( + &self, + _: &Self::Context, + _: &Self::TypeInfo, + ) -> String { + #name.to_string() + } + } + } + } + + /// Returns generated code implementing [`GraphQLValueAsync`] trait for this + /// [GraphQL object][1]. + /// + /// [`GraphQLValueAsync`]: juniper::GraphQLValueAsync + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + #[must_use] + fn impl_graphql_value_async_tokens(&self) -> TokenStream { + let scalar = &self.scalar; + + let (impl_generics, where_clause) = self.impl_generics(true); + let ty = &self.ty; + + let fields_resolvers = self + .fields + .iter() + .map(|f| f.method_resolve_field_async_tokens(scalar, None)); + let no_field_panic = field::Definition::method_resolve_field_panic_no_field_tokens(scalar); + + quote! { + #[allow(deprecated, non_snake_case)] + #[automatically_derived] + impl#impl_generics ::juniper::GraphQLValueAsync<#scalar> for #ty #where_clause + { + fn resolve_field_async<'b>( + &'b self, + info: &'b Self::TypeInfo, + field: &'b str, + args: &'b ::juniper::Arguments<#scalar>, + executor: &'b ::juniper::Executor, + ) -> ::juniper::BoxFuture<'b, ::juniper::ExecutionResult<#scalar>> { + match field { + #( #fields_resolvers )* + _ => #no_field_panic, + } + } + } + } + } + + /// Returns generated code implementing [`AsDynGraphQLValue`] trait for this + /// [GraphQL object][1]. + /// + /// [`AsDynGraphQLValue`]: juniper::AsDynGraphQLValue + /// [1]: https://spec.graphql.org/June2018/#sec-Objects + #[must_use] + fn impl_as_dyn_graphql_value_tokens(&self) -> Option { + if self.interfaces.is_empty() { + return None; + } + + let scalar = &self.scalar; + + let (impl_generics, where_clause) = self.impl_generics(true); + let ty = &self.ty; + + Some(quote! { + #[allow(non_snake_case)] + #[automatically_derived] + impl#impl_generics ::juniper::AsDynGraphQLValue<#scalar> for #ty #where_clause + { + type Context = >::Context; + type TypeInfo = >::TypeInfo; + + fn as_dyn_graphql_value( + &self, + ) -> &::juniper::DynGraphQLValue<#scalar, Self::Context, Self::TypeInfo> { + self + } + + fn as_dyn_graphql_value_async( + &self, + ) -> &::juniper::DynGraphQLValueAsync<#scalar, Self::Context, Self::TypeInfo> { + self + } + } + }) + } +} diff --git a/juniper_codegen/src/graphql_subscription/attr.rs b/juniper_codegen/src/graphql_subscription/attr.rs new file mode 100644 index 000000000..288baca0a --- /dev/null +++ b/juniper_codegen/src/graphql_subscription/attr.rs @@ -0,0 +1,29 @@ +//! Code generation for `#[graphql_subscription]` macro. + +use proc_macro2::{Span, TokenStream}; + +use crate::{ + common::parse, + graphql_object::{attr::expand_on_impl, Attr}, +}; + +use super::Subscription; + +/// Expands `#[graphql_subscription]` macro into generated code. +pub fn expand(attr_args: TokenStream, body: TokenStream) -> syn::Result { + if let Ok(mut ast) = syn::parse2::(body) { + if ast.trait_.is_none() { + let impl_attrs = parse::attr::unite(("graphql_subscription", &attr_args), &ast.attrs); + ast.attrs = parse::attr::strip("graphql_subscription", ast.attrs); + return expand_on_impl::( + Attr::from_attrs("graphql_subscription", &impl_attrs)?, + ast, + ); + } + } + + Err(syn::Error::new( + Span::call_site(), + "#[graphql_subscription] attribute is applicable to non-trait `impl` blocks only", + )) +} diff --git a/juniper_codegen/src/graphql_subscription/mod.rs b/juniper_codegen/src/graphql_subscription/mod.rs new file mode 100644 index 000000000..8f6b0e06f --- /dev/null +++ b/juniper_codegen/src/graphql_subscription/mod.rs @@ -0,0 +1,139 @@ +//! Code generation for [GraphQL subscription][1]. +//! +//! [1]: https://spec.graphql.org/June2018/#sec-Subscription + +pub mod attr; + +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::parse_quote; + +use crate::{common::field, graphql_object::Definition}; + +/// [GraphQL subscription operation][2] of the [`Definition`] to generate code +/// for. +/// +/// [2]: https://spec.graphql.org/June2018/#sec-Subscription +struct Subscription; + +impl ToTokens for Definition { + fn to_tokens(&self, into: &mut TokenStream) { + self.impl_output_type_tokens().to_tokens(into); + self.impl_graphql_type_tokens().to_tokens(into); + self.impl_graphql_value_tokens().to_tokens(into); + self.impl_graphql_subscription_value_tokens() + .to_tokens(into); + } +} + +impl Definition { + /// Returns generated code implementing [`GraphQLValue`] trait for this + /// [GraphQL subscription][1]. + /// + /// [`GraphQLValue`]: juniper::GraphQLValue + /// [1]: https://spec.graphql.org/June2018/#sec-Subscription + #[must_use] + fn impl_graphql_value_tokens(&self) -> TokenStream { + let scalar = &self.scalar; + let context = &self.context; + + let (impl_generics, where_clause) = self.impl_generics(false); + let ty = &self.ty; + + let name = &self.name; + + quote! { + #[automatically_derived] + impl#impl_generics ::juniper::GraphQLValue<#scalar> for #ty #where_clause + { + type Context = #context; + type TypeInfo = (); + + fn type_name<'__i>(&self, info: &'__i Self::TypeInfo) -> Option<&'__i str> { + >::name(info) + } + + fn resolve_field( + &self, + _: &Self::TypeInfo, + _: &str, + _: &::juniper::Arguments<#scalar>, + _: &::juniper::Executor, + ) -> ::juniper::ExecutionResult<#scalar> { + panic!("Called `resolve_field` on subscription object"); + } + + fn concrete_type_name( + &self, + _: &Self::Context, + _: &Self::TypeInfo, + ) -> String { + #name.to_string() + } + } + } + } + + /// Returns generated code implementing [`GraphQLSubscriptionValue`] trait + /// for this [GraphQL subscription][1]. + /// + /// [`GraphQLSubscriptionValue`]: juniper::GraphQLSubscriptionValue + /// [1]: https://spec.graphql.org/June2018/#sec-Subscription + #[must_use] + fn impl_graphql_subscription_value_tokens(&self) -> TokenStream { + let scalar = &self.scalar; + + // We use `for_async = false` here as `GraphQLSubscriptionValue` requires + // simpler and less `Send`/`Sync` bounds than `GraphQLValueAsync`. + let (impl_generics, mut where_clause) = self.impl_generics(false); + if scalar.is_generic() { + where_clause = Some(where_clause.unwrap_or_else(|| parse_quote! { where })); + where_clause + .as_mut() + .unwrap() + .predicates + .push(parse_quote! { #scalar: Send + Sync }); + } + let ty = &self.ty; + + let fields_resolvers = self + .fields + .iter() + .map(|f| f.method_resolve_field_into_stream_tokens(scalar)); + let no_field_panic = field::Definition::method_resolve_field_panic_no_field_tokens(scalar); + + quote! { + #[allow(deprecated)] + #[automatically_derived] + impl#impl_generics ::juniper::GraphQLSubscriptionValue<#scalar> for #ty #where_clause + { + fn resolve_field_into_stream< + 's, 'i, 'fi, 'args, 'e, 'ref_e, 'res, 'f, + >( + &'s self, + info: &'i Self::TypeInfo, + field: &'fi str, + args: ::juniper::Arguments<'args, #scalar>, + executor: &'ref_e ::juniper::Executor<'ref_e, 'e, Self::Context, #scalar>, + ) -> ::juniper::BoxFuture<'f, std::result::Result< + ::juniper::Value<::juniper::ValuesStream<'res, #scalar>>, + ::juniper::FieldError<#scalar>, + >> + where + 's: 'f, + 'fi: 'f, + 'args: 'f, + 'ref_e: 'f, + 'res: 'f, + 'i: 'res, + 'e: 'res, + { + match field { + #( #fields_resolvers )* + _ => #no_field_panic, + } + } + } + } + } +} diff --git a/juniper_codegen/src/graphql_union/attr.rs b/juniper_codegen/src/graphql_union/attr.rs index c2617d1d2..ae2be4db4 100644 --- a/juniper_codegen/src/graphql_union/attr.rs +++ b/juniper_codegen/src/graphql_union/attr.rs @@ -7,14 +7,14 @@ use quote::{quote, ToTokens as _}; use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned as _}; use crate::{ - common::parse, + common::{parse, scalar}, result::GraphQLScope, util::{path_eq_single, span_container::SpanContainer}, }; use super::{ - all_variants_different, emerge_union_variants_from_meta, UnionDefinition, UnionMeta, - UnionVariantDefinition, UnionVariantMeta, + all_variants_different, emerge_union_variants_from_attr, Attr, Definition, VariantAttr, + VariantDefinition, }; /// [`GraphQLScope`] of errors for `#[graphql_union]` macro. @@ -22,28 +22,36 @@ const ERR: GraphQLScope = GraphQLScope::UnionAttr; /// Expands `#[graphql_union]` macro into generated code. pub fn expand(attr_args: TokenStream, body: TokenStream) -> syn::Result { - let mut ast = syn::parse2::(body).map_err(|_| { - syn::Error::new( - Span::call_site(), - "#[graphql_union] attribute is applicable to trait definitions only", - ) - })?; - let trait_attrs = parse::attr::unite(("graphql_union", &attr_args), &ast.attrs); - ast.attrs = parse::attr::strip("graphql_union", ast.attrs); + if let Ok(mut ast) = syn::parse2::(body) { + let trait_attrs = parse::attr::unite(("graphql_union", &attr_args), &ast.attrs); + ast.attrs = parse::attr::strip("graphql_union", ast.attrs); + return expand_on_trait(trait_attrs, ast); + } + + Err(syn::Error::new( + Span::call_site(), + "#[graphql_union] attribute is applicable to trait definitions only", + )) +} - let meta = UnionMeta::from_attrs("graphql_union", &trait_attrs)?; +/// Expands `#[graphql_union]` macro placed on a trait definition. +fn expand_on_trait( + attrs: Vec, + mut ast: syn::ItemTrait, +) -> syn::Result { + let attr = Attr::from_attrs("graphql_union", &attrs)?; let trait_span = ast.span(); let trait_ident = &ast.ident; - let name = meta + let name = attr .name .clone() .map(SpanContainer::into_inner) .unwrap_or_else(|| trait_ident.unraw().to_string()); - if !meta.is_internal && name.starts_with("__") { + if !attr.is_internal && name.starts_with("__") { ERR.no_double_underscore( - meta.name + attr.name .as_ref() .map(SpanContainer::span_ident) .unwrap_or_else(|| trait_ident.span()), @@ -54,14 +62,14 @@ pub fn expand(attr_args: TokenStream, body: TokenStream) -> syn::Result parse_variant_from_trait_method(m, trait_ident, &meta), + syn::TraitItem::Method(m) => parse_variant_from_trait_method(m, trait_ident, &attr), _ => None, }) .collect(); proc_macro_error::abort_if_dirty(); - emerge_union_variants_from_meta(&mut variants, meta.external_resolvers); + emerge_union_variants_from_attr(&mut variants, attr.external_resolvers); if variants.is_empty() { ERR.emit_custom(trait_span, "expects at least one union variant"); @@ -76,40 +84,40 @@ pub fn expand(attr_args: TokenStream, body: TokenStream) -> syn::Result Option { + trait_attr: &Attr, +) -> Option { let method_attrs = method.attrs.clone(); // Remove repeated attributes from the method, to omit incorrect expansion. @@ -118,22 +126,22 @@ fn parse_variant_from_trait_method( .filter(|attr| !path_eq_single(&attr.path, "graphql")) .collect(); - let meta = UnionVariantMeta::from_attrs("graphql", &method_attrs) + let attr = VariantAttr::from_attrs("graphql", &method_attrs) .map_err(|e| proc_macro_error::emit_error!(e)) .ok()?; - if let Some(rslvr) = meta.external_resolver { + if let Some(rslvr) = attr.external_resolver { ERR.custom( rslvr.span_ident(), "cannot use #[graphql(with = ...)] attribute on a trait method", ) .note(String::from( - "instead use #[graphql(ignore)] on the method with #[graphql_union(on ... = ...)] on \ - the trait itself", + "instead use #[graphql(ignore)] on the method with \ + #[graphql_union(on ... = ...)] on the trait itself", )) .emit() } - if meta.ignore.is_some() { + if attr.ignore.is_some() { return None; } @@ -165,21 +173,21 @@ fn parse_variant_from_trait_method( } let resolver_code = { - if let Some(other) = trait_meta.external_resolvers.get(&ty) { + if let Some(other) = trait_attr.external_resolvers.get(&ty) { ERR.custom( method_span, format!( - "trait method `{}` conflicts with the external resolver function `{}` declared \ - on the trait to resolve the variant type `{}`", + "trait method `{}` conflicts with the external resolver \ + function `{}` declared on the trait to resolve the \ + variant type `{}`", method_ident, other.to_token_stream(), ty.to_token_stream(), - ), ) .note(String::from( - "use `#[graphql(ignore)]` attribute to ignore this trait method for union \ - variants resolution", + "use `#[graphql(ignore)]` attribute to ignore this trait \ + method for union variants resolution", )) .emit(); } @@ -195,17 +203,18 @@ fn parse_variant_from_trait_method( } }; - // Doing this may be quite an expensive, because resolving may contain some heavy computation, - // so we're preforming it twice. Unfortunately, we have no other options here, until the - // `juniper::GraphQLType` itself will allow to do it in some cleverer way. + // Doing this may be quite an expensive, because resolving may contain some + // heavy computation, so we're preforming it twice. Unfortunately, we have + // no other options here, until the `juniper::GraphQLType` itself will allow + // to do it in some cleverer way. let resolver_check = parse_quote! { ({ #resolver_code } as ::std::option::Option<&#ty>).is_some() }; - Some(UnionVariantDefinition { + Some(VariantDefinition { ty, resolver_code, resolver_check, - context_ty: method_context_ty, + context: method_context_ty, }) } diff --git a/juniper_codegen/src/graphql_union/derive.rs b/juniper_codegen/src/graphql_union/derive.rs index 161ddf6a9..1c1bc5c1d 100644 --- a/juniper_codegen/src/graphql_union/derive.rs +++ b/juniper_codegen/src/graphql_union/derive.rs @@ -6,12 +6,14 @@ use quote::{quote, ToTokens}; use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned as _, Data, Fields}; use crate::{ - common::parse::TypeExt as _, result::GraphQLScope, util::span_container::SpanContainer, + common::{parse::TypeExt as _, scalar}, + result::GraphQLScope, + util::span_container::SpanContainer, }; use super::{ - all_variants_different, emerge_union_variants_from_meta, UnionDefinition, UnionMeta, - UnionVariantDefinition, UnionVariantMeta, + all_variants_different, emerge_union_variants_from_attr, Attr, Definition, VariantAttr, + VariantDefinition, }; /// [`GraphQLScope`] of errors for `#[derive(GraphQLUnion)]` macro. @@ -29,21 +31,22 @@ pub fn expand(input: TokenStream) -> syn::Result { .map(ToTokens::into_token_stream) } -/// Expands into generated code a `#[derive(GraphQLUnion)]` macro placed on a Rust enum. -fn expand_enum(ast: syn::DeriveInput) -> syn::Result { - let meta = UnionMeta::from_attrs("graphql", &ast.attrs)?; +/// Expands into generated code a `#[derive(GraphQLUnion)]` macro placed on a +/// Rust enum. +fn expand_enum(ast: syn::DeriveInput) -> syn::Result { + let attr = Attr::from_attrs("graphql", &ast.attrs)?; let enum_span = ast.span(); let enum_ident = ast.ident; - let name = meta + let name = attr .name .clone() .map(SpanContainer::into_inner) .unwrap_or_else(|| enum_ident.unraw().to_string()); - if !meta.is_internal && name.starts_with("__") { + if !attr.is_internal && name.starts_with("__") { ERR.no_double_underscore( - meta.name + attr.name .as_ref() .map(SpanContainer::span_ident) .unwrap_or_else(|| enum_ident.span()), @@ -55,12 +58,12 @@ fn expand_enum(ast: syn::DeriveInput) -> syn::Result { _ => unreachable!(), } .into_iter() - .filter_map(|var| parse_variant_from_enum_variant(var, &enum_ident, &meta)) + .filter_map(|var| parse_variant_from_enum_variant(var, &enum_ident, &attr)) .collect(); proc_macro_error::abort_if_dirty(); - emerge_union_variants_from_meta(&mut variants, meta.external_resolvers); + emerge_union_variants_from_attr(&mut variants, attr.external_resolvers); if variants.is_empty() { ERR.emit_custom(enum_span, "expects at least one union variant"); @@ -75,13 +78,16 @@ fn expand_enum(ast: syn::DeriveInput) -> syn::Result { proc_macro_error::abort_if_dirty(); - Ok(UnionDefinition { + Ok(Definition { name, ty: parse_quote! { #enum_ident }, is_trait_object: false, - description: meta.description.map(SpanContainer::into_inner), - context: meta.context.map(SpanContainer::into_inner), - scalar: meta.scalar.map(SpanContainer::into_inner), + description: attr.description.map(SpanContainer::into_inner), + context: attr + .context + .map(SpanContainer::into_inner) + .unwrap_or_else(|| parse_quote! { () }), + scalar: scalar::Type::parse(attr.scalar.as_deref(), &ast.generics), generics: ast.generics, variants, }) @@ -89,19 +95,19 @@ fn expand_enum(ast: syn::DeriveInput) -> syn::Result { /// Parses given Rust enum `var`iant as [GraphQL union][1] variant. /// -/// On failure returns [`None`] and internally fills up [`proc_macro_error`] with the corresponding -/// errors. +/// On failure returns [`None`] and internally fills up [`proc_macro_error`] +/// with the corresponding errors. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions fn parse_variant_from_enum_variant( var: syn::Variant, enum_ident: &syn::Ident, - enum_meta: &UnionMeta, -) -> Option { - let meta = UnionVariantMeta::from_attrs("graphql", &var.attrs) + enum_attr: &Attr, +) -> Option { + let attr = VariantAttr::from_attrs("graphql", &var.attrs) .map_err(|e| proc_macro_error::emit_error!(e)) .ok()?; - if meta.ignore.is_some() { + if attr.ignore.is_some() { return None; } @@ -129,12 +135,13 @@ fn parse_variant_from_enum_variant( let enum_path = quote! { #enum_ident::#var_ident }; - let resolver_code = if let Some(rslvr) = meta.external_resolver { - if let Some(other) = enum_meta.external_resolvers.get(&ty) { + let resolver_code = if let Some(rslvr) = attr.external_resolver { + if let Some(other) = enum_attr.external_resolvers.get(&ty) { ERR.emit_custom( rslvr.span_ident(), format!( - "variant `{}` already has external resolver function `{}` declared on the enum", + "variant `{}` already has external resolver function `{}` \ + declared on the enum", ty.to_token_stream(), other.to_token_stream(), ), @@ -156,29 +163,30 @@ fn parse_variant_from_enum_variant( matches!(self, #enum_path(_)) }; - Some(UnionVariantDefinition { + Some(VariantDefinition { ty, resolver_code, resolver_check, - context_ty: None, + context: None, }) } -/// Expands into generated code a `#[derive(GraphQLUnion)]` macro placed on a Rust struct. -fn expand_struct(ast: syn::DeriveInput) -> syn::Result { - let meta = UnionMeta::from_attrs("graphql", &ast.attrs)?; +/// Expands into generated code a `#[derive(GraphQLUnion)]` macro placed on a +/// Rust struct. +fn expand_struct(ast: syn::DeriveInput) -> syn::Result { + let attr = Attr::from_attrs("graphql", &ast.attrs)?; let struct_span = ast.span(); let struct_ident = ast.ident; - let name = meta + let name = attr .name .clone() .map(SpanContainer::into_inner) .unwrap_or_else(|| struct_ident.unraw().to_string()); - if !meta.is_internal && name.starts_with("__") { + if !attr.is_internal && name.starts_with("__") { ERR.no_double_underscore( - meta.name + attr.name .as_ref() .map(SpanContainer::span_ident) .unwrap_or_else(|| struct_ident.span()), @@ -186,7 +194,7 @@ fn expand_struct(ast: syn::DeriveInput) -> syn::Result { } let mut variants = vec![]; - emerge_union_variants_from_meta(&mut variants, meta.external_resolvers); + emerge_union_variants_from_attr(&mut variants, attr.external_resolvers); if variants.is_empty() { ERR.emit_custom(struct_span, "expects at least one union variant"); } @@ -200,13 +208,16 @@ fn expand_struct(ast: syn::DeriveInput) -> syn::Result { proc_macro_error::abort_if_dirty(); - Ok(UnionDefinition { + Ok(Definition { name, ty: parse_quote! { #struct_ident }, is_trait_object: false, - description: meta.description.map(SpanContainer::into_inner), - context: meta.context.map(SpanContainer::into_inner), - scalar: meta.scalar.map(SpanContainer::into_inner), + description: attr.description.map(SpanContainer::into_inner), + context: attr + .context + .map(SpanContainer::into_inner) + .unwrap_or_else(|| parse_quote! { () }), + scalar: scalar::Type::parse(attr.scalar.as_deref(), &ast.generics), generics: ast.generics, variants, }) diff --git a/juniper_codegen/src/graphql_union/mod.rs b/juniper_codegen/src/graphql_union/mod.rs index d34541708..c882c24ba 100644 --- a/juniper_codegen/src/graphql_union/mod.rs +++ b/juniper_codegen/src/graphql_union/mod.rs @@ -8,8 +8,9 @@ pub mod derive; use std::collections::HashMap; use proc_macro2::TokenStream; -use quote::{quote, ToTokens, TokenStreamExt as _}; +use quote::{format_ident, quote, ToTokens}; use syn::{ + ext::IdentExt as _, parse::{Parse, ParseStream}, parse_quote, spanned::Spanned as _, @@ -17,83 +18,90 @@ use syn::{ }; use crate::{ - common::parse::{ - attr::{err, OptionExt as _}, - ParseBufferExt as _, + common::{ + gen, + parse::{ + attr::{err, OptionExt as _}, + ParseBufferExt as _, + }, + scalar, }, util::{filter_attrs, get_doc_comment, span_container::SpanContainer}, }; -/// Helper alias for the type of [`UnionMeta::external_resolvers`] field. -type UnionMetaResolvers = HashMap>; +/// Helper alias for the type of [`Attr::external_resolvers`] field. +type AttrResolvers = HashMap>; -/// Available metadata (arguments) behind `#[graphql]` (or `#[graphql_union]`) attribute when -/// generating code for [GraphQL union][1] type. +/// Available arguments behind `#[graphql]` (or `#[graphql_union]`) attribute +/// when generating code for [GraphQL union][1] type. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions #[derive(Debug, Default)] -struct UnionMeta { +struct Attr { /// Explicitly specified name of [GraphQL union][1] type. /// - /// If absent, then Rust type name is used by default. + /// If [`None`], then Rust type name is used by default. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub name: Option>, + name: Option>, /// Explicitly specified [description][2] of [GraphQL union][1] type. /// - /// If absent, then Rust doc comment is used as [description][2], if any. + /// If [`None`], then Rust doc comment is used as [description][2], if any. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions /// [2]: https://spec.graphql.org/June2018/#sec-Descriptions - pub description: Option>, + description: Option>, - /// Explicitly specified type of `juniper::Context` to use for resolving this [GraphQL union][1] - /// type with. + /// Explicitly specified type of [`Context`] to use for resolving this + /// [GraphQL union][1] type with. /// - /// If absent, then unit type `()` is assumed as type of `juniper::Context`. + /// If [`None`], then unit type `()` is assumed as a type of [`Context`]. /// + /// [`Context`]: juniper::Context /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub context: Option>, + context: Option>, - /// Explicitly specified type of `juniper::ScalarValue` to use for resolving this + /// Explicitly specified type of [`ScalarValue`] to use for resolving this /// [GraphQL union][1] type with. /// - /// If absent, then generated code will be generic over any `juniper::ScalarValue` type, which, - /// in turn, requires all [union][1] variants to be generic over any `juniper::ScalarValue` type - /// too. That's why this type should be specified only if one of the variants implements - /// `juniper::GraphQLType` in a non-generic way over `juniper::ScalarValue` type. + /// If [`None`], then generated code will be generic over any + /// [`ScalarValue`] type, which, in turn, requires all [union][1] variants + /// to be generic over any [`ScalarValue`] type too. That's why this type + /// should be specified only if one of the variants implements + /// [`GraphQLType`] in a non-generic way over [`ScalarValue`] type. /// + /// [`GraphQLType`]: juniper::GraphQLType + /// [`ScalarValue`]: juniper::ScalarValue /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub scalar: Option>, + scalar: Option>, - /// Explicitly specified external resolver functions for [GraphQL union][1] variants. + /// Explicitly specified external resolver functions for [GraphQL union][1] + /// variants. /// - /// If absent, then macro will try to auto-infer all the possible variants from the type - /// declaration, if possible. That's why specifying an external resolver function has sense, - /// when some custom [union][1] variant resolving logic is involved, or variants cannot be - /// inferred. + /// If [`None`], then macro will try to auto-infer all the possible variants + /// from the type declaration, if possible. That's why specifying an + /// external resolver function has sense, when some custom [union][1] + /// variant resolving logic is involved, or variants cannot be inferred. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub external_resolvers: UnionMetaResolvers, + external_resolvers: AttrResolvers, - /// Indicator whether the generated code is intended to be used only inside the `juniper` - /// library. - pub is_internal: bool, + /// Indicator whether the generated code is intended to be used only inside + /// the [`juniper`] library. + is_internal: bool, } -impl Parse for UnionMeta { - fn parse(input: ParseStream) -> syn::Result { - let mut output = Self::default(); - +impl Parse for Attr { + fn parse(input: ParseStream<'_>) -> syn::Result { + let mut out = Self::default(); while !input.is_empty() { let ident = input.parse::()?; match ident.to_string().as_str() { "name" => { input.parse::()?; let name = input.parse::()?; - output - .name + out.name .replace(SpanContainer::new( ident.span(), Some(name.span()), @@ -104,8 +112,7 @@ impl Parse for UnionMeta { "desc" | "description" => { input.parse::()?; let desc = input.parse::()?; - output - .description + out.description .replace(SpanContainer::new( ident.span(), Some(desc.span()), @@ -116,16 +123,14 @@ impl Parse for UnionMeta { "ctx" | "context" | "Context" => { input.parse::()?; let ctx = input.parse::()?; - output - .context + out.context .replace(SpanContainer::new(ident.span(), Some(ctx.span()), ctx)) .none_or_else(|_| err::dup_arg(&ident))? } "scalar" | "Scalar" | "ScalarValue" => { input.parse::()?; - let scl = input.parse::()?; - output - .scalar + let scl = input.parse::()?; + out.scalar .replace(SpanContainer::new(ident.span(), Some(scl.span()), scl)) .none_or_else(|_| err::dup_arg(&ident))? } @@ -135,13 +140,12 @@ impl Parse for UnionMeta { let rslvr = input.parse::()?; let rslvr_spanned = SpanContainer::new(ident.span(), Some(ty.span()), rslvr); let rslvr_span = rslvr_spanned.span_joined(); - output - .external_resolvers + out.external_resolvers .insert(ty, rslvr_spanned) .none_or_else(|_| err::dup_arg(rslvr_span))? } "internal" => { - output.is_internal = true; + out.is_internal = true; } name => { return Err(err::unknown_arg(&ident, name)); @@ -149,13 +153,13 @@ impl Parse for UnionMeta { } input.try_parse::()?; } - - Ok(output) + Ok(out) } } -impl UnionMeta { - /// Tries to merge two [`UnionMeta`]s into a single one, reporting about duplicates, if any. +impl Attr { + /// Tries to merge two [`Attr`]s into a single one, reporting about + /// duplicates, if any. fn try_merge(self, mut another: Self) -> syn::Result { Ok(Self { name: try_merge_opt!(name: self, another), @@ -169,9 +173,9 @@ impl UnionMeta { }) } - /// Parses [`UnionMeta`] from the given multiple `name`d [`syn::Attribute`]s placed on a type - /// definition. - pub fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result { + /// Parses [`Attr`] from the given multiple `name`d [`syn::Attribute`]s + /// placed on a type definition. + fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result { let mut meta = filter_attrs(name, attrs) .map(|attr| attr.parse_args()) .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?; @@ -184,17 +188,17 @@ impl UnionMeta { } } -/// Available metadata (arguments) behind `#[graphql]` (or `#[graphql_union]`) attribute when -/// generating code for [GraphQL union][1]'s variant. +/// Available arguments behind `#[graphql]` attribute when generating code for +/// [GraphQL union][1]'s variant. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions #[derive(Debug, Default)] -struct UnionVariantMeta { - /// Explicitly specified marker for the variant/field being ignored and not included into - /// [GraphQL union][1]. +struct VariantAttr { + /// Explicitly specified marker for the variant/field being ignored and not + /// included into [GraphQL union][1]. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub ignore: Option>, + ignore: Option>, /// Explicitly specified external resolver function for this [GraphQL union][1] variant. /// @@ -203,25 +207,23 @@ struct UnionVariantMeta { /// logic is involved. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub external_resolver: Option>, + external_resolver: Option>, } -impl Parse for UnionVariantMeta { - fn parse(input: ParseStream) -> syn::Result { - let mut output = Self::default(); - +impl Parse for VariantAttr { + fn parse(input: ParseStream<'_>) -> syn::Result { + let mut out = Self::default(); while !input.is_empty() { let ident = input.parse::()?; match ident.to_string().as_str() { - "ignore" | "skip" => output + "ignore" | "skip" => out .ignore .replace(SpanContainer::new(ident.span(), None, ident.clone())) .none_or_else(|_| err::dup_arg(&ident))?, "with" => { input.parse::()?; let rslvr = input.parse::()?; - output - .external_resolver + out.external_resolver .replace(SpanContainer::new(ident.span(), Some(rslvr.span()), rslvr)) .none_or_else(|_| err::dup_arg(&ident))? } @@ -231,14 +233,13 @@ impl Parse for UnionVariantMeta { } input.try_parse::()?; } - - Ok(output) + Ok(out) } } -impl UnionVariantMeta { - /// Tries to merge two [`UnionVariantMeta`]s into a single one, reporting about duplicates, if - /// any. +impl VariantAttr { + /// Tries to merge two [`VariantAttr`]s into a single one, reporting about + /// duplicates, if any. fn try_merge(self, mut another: Self) -> syn::Result { Ok(Self { ignore: try_merge_opt!(ignore: self, another), @@ -246,219 +247,238 @@ impl UnionVariantMeta { }) } - /// Parses [`UnionVariantMeta`] from the given multiple `name`d [`syn::Attribute`]s placed on a - /// variant/field/method definition. - pub fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result { + /// Parses [`VariantAttr`] from the given multiple `name`d + /// [`syn::Attribute`]s placed on a variant/field/method definition. + fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result { filter_attrs(name, attrs) .map(|attr| attr.parse_args()) .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?)) } } -/// Definition of [GraphQL union][1] variant for code generation. -/// -/// [1]: https://spec.graphql.org/June2018/#sec-Unions -struct UnionVariantDefinition { - /// Rust type that this [GraphQL union][1] variant resolves into. - /// - /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub ty: syn::Type, - - /// Rust code for value resolution of this [GraphQL union][1] variant. - /// - /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub resolver_code: syn::Expr, - - /// Rust code for checking whether [GraphQL union][1] should be resolved into this variant. - /// - /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub resolver_check: syn::Expr, - - /// Rust type of `juniper::Context` that this [GraphQL union][1] variant requires for - /// resolution. - /// - /// It's available only when code generation happens for Rust traits and a trait method contains - /// context argument. - /// - /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub context_ty: Option, -} - /// Definition of [GraphQL union][1] for code generation. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions -struct UnionDefinition { +struct Definition { /// Name of this [GraphQL union][1] in GraphQL schema. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub name: String, + name: String, /// Rust type that this [GraphQL union][1] is represented with. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub ty: syn::Type, + ty: syn::Type, - /// Generics of the Rust type that this [GraphQL union][1] is implemented for. + /// Generics of the Rust type that this [GraphQL union][1] is implemented + /// for. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub generics: syn::Generics, + generics: syn::Generics, - /// Indicator whether code should be generated for a trait object, rather than for a regular - /// Rust type. - pub is_trait_object: bool, + /// Indicator whether code should be generated for a trait object, rather + /// than for a regular Rust type. + is_trait_object: bool, /// Description of this [GraphQL union][1] to put into GraphQL schema. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub description: Option, + description: Option, - /// Rust type of `juniper::Context` to generate `juniper::GraphQLType` implementation with + /// Rust type of [`Context`] to generate [`GraphQLType`] implementation with /// for this [GraphQL union][1]. /// - /// If [`None`] then generated code will use unit type `()` as `juniper::Context`. - /// + /// [`Context`]: juniper::Context + /// [`GraphQLType`]: juniper::GraphQLType /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub context: Option, + context: syn::Type, - /// Rust type of `juniper::ScalarValue` to generate `juniper::GraphQLType` implementation with - /// for this [GraphQL union][1]. + /// Rust type of [`ScalarValue`] to generate [`GraphQLType`] implementation + /// with for this [GraphQL union][1]. /// - /// If [`None`] then generated code will be generic over any `juniper::ScalarValue` type, which, - /// in turn, requires all [union][1] variants to be generic over any `juniper::ScalarValue` type - /// too. That's why this type should be specified only if one of the variants implements - /// `juniper::GraphQLType` in a non-generic way over `juniper::ScalarValue` type. + /// If [`None`] then generated code will be generic over any [`ScalarValue`] + /// type, which, in turn, requires all [union][1] variants to be generic + /// over any [`ScalarValue`] type too. That's why this type should be + /// specified only if one of the variants implements [`GraphQLType`] in a + /// non-generic way over [`ScalarValue`] type. /// + /// [`GraphQLType`]: juniper::GraphQLType + /// [`ScalarValue`]: juniper::ScalarValue /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub scalar: Option, + scalar: scalar::Type, /// Variants definitions of this [GraphQL union][1]. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub variants: Vec, + variants: Vec, } -impl ToTokens for UnionDefinition { +impl ToTokens for Definition { fn to_tokens(&self, into: &mut TokenStream) { - let name = &self.name; - let ty = &self.ty; + self.impl_graphql_union_tokens().to_tokens(into); + self.impl_output_type_tokens().to_tokens(into); + self.impl_graphql_type_tokens().to_tokens(into); + self.impl_graphql_value_tokens().to_tokens(into); + self.impl_graphql_value_async_tokens().to_tokens(into); + } +} - let context = self - .context - .as_ref() - .map(|ctx| quote! { #ctx }) - .unwrap_or_else(|| quote! { () }); +impl Definition { + /// Returns prepared [`syn::Generics::split_for_impl`] for [`GraphQLType`] + /// trait (and similar) implementation of this [GraphQL union][1]. + /// + /// If `for_async` is `true`, then additional predicates are added to suit + /// the [`GraphQLAsyncValue`] trait (and similar) requirements. + /// + /// [`GraphQLAsyncValue`]: juniper::GraphQLAsyncValue + /// [`GraphQLType`]: juniper::GraphQLType + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + #[must_use] + fn impl_generics( + &self, + for_async: bool, + ) -> (TokenStream, TokenStream, Option) { + let (_, ty_generics, _) = self.generics.split_for_impl(); + let ty = &self.ty; - let scalar = self - .scalar - .as_ref() - .map(|scl| quote! { #scl }) - .unwrap_or_else(|| quote! { __S }); + let mut ty_full = quote! { #ty#ty_generics }; + if self.is_trait_object { + ty_full = quote! { dyn #ty_full + '__obj + Send + Sync }; + } - let description = self - .description - .as_ref() - .map(|desc| quote! { .description(#desc) }); + let mut generics = self.generics.clone(); - let var_types: Vec<_> = self.variants.iter().map(|var| &var.ty).collect(); + if self.is_trait_object { + generics.params.push(parse_quote! { '__obj }); + } - let all_variants_unique = if var_types.len() > 1 { - Some(quote! { ::juniper::sa::assert_type_ne_all!(#(#var_types),*); }) - } else { - None - }; + let scalar = &self.scalar; + if scalar.is_implicit_generic() { + generics.params.push(parse_quote! { #scalar }); + } + if scalar.is_generic() { + generics + .make_where_clause() + .predicates + .push(parse_quote! { #scalar: ::juniper::ScalarValue }); + } + if let Some(bound) = scalar.bounds() { + generics.make_where_clause().predicates.push(bound); + } - let match_names = self.variants.iter().map(|var| { - let var_ty = &var.ty; - let var_check = &var.resolver_check; - quote! { - if #var_check { - return <#var_ty as ::juniper::GraphQLType<#scalar>>::name(info) - .unwrap().to_string(); + if for_async { + let self_ty = if !self.is_trait_object && self.generics.lifetimes().next().is_some() { + // Modify lifetime names to omit "lifetime name `'a` shadows a + // lifetime name that is already in scope" error. + let mut generics = self.generics.clone(); + for lt in generics.lifetimes_mut() { + let ident = lt.lifetime.ident.unraw(); + lt.lifetime.ident = format_ident!("__fa__{}", ident); } + + let lifetimes = generics.lifetimes().map(|lt| <.lifetime); + let ty = &self.ty; + let (_, ty_generics, _) = generics.split_for_impl(); + + quote! { for<#( #lifetimes ),*> #ty#ty_generics } + } else { + quote! { Self } + }; + generics + .make_where_clause() + .predicates + .push(parse_quote! { #self_ty: Sync }); + + if scalar.is_generic() { + generics + .make_where_clause() + .predicates + .push(parse_quote! { #scalar: Send + Sync }); } + } + + let (impl_generics, _, where_clause) = generics.split_for_impl(); + ( + quote! { #impl_generics }, + quote! { #ty_full }, + where_clause.cloned(), + ) + } + + /// Returns generated code implementing [`GraphQLUnion`] trait for this + /// [GraphQL union][1]. + /// + /// [`GraphQLUnion`]: juniper::GraphQLUnion + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + #[must_use] + fn impl_graphql_union_tokens(&self) -> TokenStream { + let scalar = &self.scalar; + + let (impl_generics, ty_full, where_clause) = self.impl_generics(false); + + let variant_tys: Vec<_> = self.variants.iter().map(|var| &var.ty).collect(); + let all_variants_unique = (variant_tys.len() > 1).then(|| { + quote! { ::juniper::sa::assert_type_ne_all!(#( #variant_tys ),*); } }); - let match_resolves: Vec<_> = self.variants.iter().map(|var| &var.resolver_code).collect(); - let resolve_into_type = self.variants.iter().zip(match_resolves.iter()).map(|(var, expr)| { - let var_ty = &var.ty; - - let get_name = quote! { (<#var_ty as ::juniper::GraphQLType<#scalar>>::name(info)) }; - quote! { - if type_name == #get_name.unwrap() { - return ::juniper::IntoResolvable::into( - { #expr }, - executor.context(), - ) - .and_then(|res| match res { - Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(info, &r), - None => Ok(::juniper::Value::null()), - }); + quote! { + #[automatically_derived] + impl#impl_generics ::juniper::marker::GraphQLUnion<#scalar> for #ty_full #where_clause + { + fn mark() { + #all_variants_unique + #( <#variant_tys as ::juniper::marker::GraphQLObject<#scalar>>::mark(); )* } } - }); - let resolve_into_type_async = - self.variants - .iter() - .zip(match_resolves.iter()) - .map(|(var, expr)| { - let var_ty = &var.ty; - - let get_name = quote! { - (<#var_ty as ::juniper::GraphQLType<#scalar>>::name(info)) - }; - quote! { - if type_name == #get_name.unwrap() { - let res = ::juniper::IntoResolvable::into( - { #expr }, - executor.context(), - ); - return Box::pin(async move { - match res? { - Some((ctx, r)) => { - let subexec = executor.replaced_context(ctx); - subexec.resolve_with_ctx_async(info, &r).await - }, - None => Ok(::juniper::Value::null()), - } - }); - } - } - }); + } + } - let (_, ty_generics, _) = self.generics.split_for_impl(); + /// Returns generated code implementing [`marker::IsOutputType`] trait for + /// this [GraphQL union][1]. + /// + /// [`marker::IsOutputType`]: juniper::marker::IsOutputType + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + #[must_use] + fn impl_output_type_tokens(&self) -> TokenStream { + let scalar = &self.scalar; - let mut ext_generics = self.generics.clone(); - if self.is_trait_object { - ext_generics.params.push(parse_quote! { '__obj }); - } - if self.scalar.is_none() { - ext_generics.params.push(parse_quote! { #scalar }); - ext_generics - .make_where_clause() - .predicates - .push(parse_quote! { #scalar: ::juniper::ScalarValue }); - } - let (ext_impl_generics, _, where_clause) = ext_generics.split_for_impl(); - - let mut where_async = where_clause - .cloned() - .unwrap_or_else(|| parse_quote! { where }); - where_async.predicates.push(parse_quote! { Self: Sync }); - if self.scalar.is_none() { - where_async - .predicates - .push(parse_quote! { #scalar: Send + Sync }); - } + let (impl_generics, ty_full, where_clause) = self.impl_generics(false); - let mut ty_full = quote! { #ty#ty_generics }; - if self.is_trait_object { - ty_full = quote! { dyn #ty_full + '__obj + Send + Sync }; + let variant_tys = self.variants.iter().map(|var| &var.ty); + + quote! { + #[automatically_derived] + impl#impl_generics ::juniper::marker::IsOutputType<#scalar> for #ty_full #where_clause + { + fn mark() { + #( <#variant_tys as ::juniper::marker::IsOutputType<#scalar>>::mark(); )* + } + } } + } + + /// Returns generated code implementing [`GraphQLType`] trait for this + /// [GraphQL union][1]. + /// + /// [`GraphQLType`]: juniper::GraphQLType + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + #[must_use] + fn impl_graphql_type_tokens(&self) -> TokenStream { + let scalar = &self.scalar; + + let (impl_generics, ty_full, where_clause) = self.impl_generics(false); - let type_impl = quote! { + let name = &self.name; + let description = self + .description + .as_ref() + .map(|desc| quote! { .description(#desc) }); + + let variant_tys = self.variants.iter().map(|var| &var.ty); + + quote! { #[automatically_derived] - impl#ext_impl_generics ::juniper::GraphQLType<#scalar> for #ty_full - #where_clause + impl#impl_generics ::juniper::GraphQLType<#scalar> for #ty_full #where_clause { fn name(_ : &Self::TypeInfo) -> Option<&'static str> { Some(#name) @@ -471,19 +491,43 @@ impl ToTokens for UnionDefinition { where #scalar: 'r, { let types = [ - #( registry.get_type::<#var_types>(info), )* + #( registry.get_type::<#variant_tys>(info), )* ]; registry.build_union_type::<#ty_full>(info, &types) - #description - .into_meta() + #description + .into_meta() } } - }; + } + } - let value_impl = quote! { + /// Returns generated code implementing [`GraphQLValue`] trait for this + /// [GraphQL union][1]. + /// + /// [`GraphQLValue`]: juniper::GraphQLValue + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + #[must_use] + fn impl_graphql_value_tokens(&self) -> TokenStream { + let scalar = &self.scalar; + let context = &self.context; + + let (impl_generics, ty_full, where_clause) = self.impl_generics(false); + + let name = &self.name; + + let match_variant_names = self + .variants + .iter() + .map(|v| v.method_concrete_type_name_tokens(scalar)); + + let variant_resolvers = self + .variants + .iter() + .map(|v| v.method_resolve_into_type_tokens(scalar)); + + quote! { #[automatically_derived] - impl#ext_impl_generics ::juniper::GraphQLValue<#scalar> for #ty_full - #where_clause + impl#impl_generics ::juniper::GraphQLValue<#scalar> for #ty_full #where_clause { type Context = #context; type TypeInfo = (); @@ -497,10 +541,10 @@ impl ToTokens for UnionDefinition { context: &Self::Context, info: &Self::TypeInfo, ) -> String { - #( #match_names )* + #( #match_variant_names )* panic!( - "GraphQL union {} cannot be resolved into any of its variants in its \ - current state", + "GraphQL union `{}` cannot be resolved into any of its \ + variants in its current state", #name, ); } @@ -513,19 +557,39 @@ impl ToTokens for UnionDefinition { executor: &::juniper::Executor, ) -> ::juniper::ExecutionResult<#scalar> { let context = executor.context(); - #( #resolve_into_type )* + #( #variant_resolvers )* panic!( - "Concrete type {} is not handled by instance resolvers on GraphQL union {}", + "Concrete type `{}` is not handled by instance \ + resolvers on GraphQL union `{}`", type_name, #name, ); } } - }; + } + } - let value_async_impl = quote! { + /// Returns generated code implementing [`GraphQLValueAsync`] trait for this + /// [GraphQL union][1]. + /// + /// [`GraphQLValueAsync`]: juniper::GraphQLValueAsync + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + #[must_use] + fn impl_graphql_value_async_tokens(&self) -> TokenStream { + let scalar = &self.scalar; + + let (impl_generics, ty_full, where_clause) = self.impl_generics(true); + + let name = &self.name; + + let variant_async_resolvers = self + .variants + .iter() + .map(|v| v.method_resolve_into_type_async_tokens(scalar)); + + quote! { + #[allow(non_snake_case)] #[automatically_derived] - impl#ext_impl_generics ::juniper::GraphQLValueAsync<#scalar> for #ty_full - #where_async + impl#impl_generics ::juniper::GraphQLValueAsync<#scalar> for #ty_full #where_clause { fn resolve_into_type_async<'b>( &'b self, @@ -535,58 +599,119 @@ impl ToTokens for UnionDefinition { executor: &'b ::juniper::Executor<'b, 'b, Self::Context, #scalar> ) -> ::juniper::BoxFuture<'b, ::juniper::ExecutionResult<#scalar>> { let context = executor.context(); - #( #resolve_into_type_async )* + #( #variant_async_resolvers )* panic!( - "Concrete type {} is not handled by instance resolvers on GraphQL union {}", + "Concrete type `{}` is not handled by instance \ + resolvers on GraphQL union `{}`", type_name, #name, ); } } - }; + } + } +} - let output_type_impl = quote! { - #[automatically_derived] - impl#ext_impl_generics ::juniper::marker::IsOutputType<#scalar> for #ty_full - #where_clause - { - fn mark() { - #( <#var_types as ::juniper::marker::IsOutputType<#scalar>>::mark(); )* - } +/// Definition of [GraphQL union][1] variant for code generation. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +struct VariantDefinition { + /// Rust type that this [GraphQL union][1] variant resolves into. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + ty: syn::Type, + + /// Rust code for value resolution of this [GraphQL union][1] variant. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + resolver_code: syn::Expr, + + /// Rust code for checking whether [GraphQL union][1] should be resolved + /// into this variant. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + resolver_check: syn::Expr, + + /// Rust type of [`Context`] that this [GraphQL union][1] variant requires + /// for resolution. + /// + /// It's available only when code generation happens for Rust traits and a + /// trait method contains context argument. + /// + /// [`Context`]: juniper::Context + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + context: Option, +} + +impl VariantDefinition { + /// Returns generated code for the [`GraphQLValue::concrete_type_name`][0] + /// method, which returns name of the underlying GraphQL type contained in + /// this [`VariantDefinition`]. + /// + /// [0]: juniper::GraphQLValue::concrete_type_name + #[must_use] + fn method_concrete_type_name_tokens(&self, scalar: &scalar::Type) -> TokenStream { + let ty = &self.ty; + let check = &self.resolver_check; + + quote! { + if #check { + return <#ty as ::juniper::GraphQLType<#scalar>>::name(info) + .unwrap() + .to_string(); } - }; + } + } - let union_impl = quote! { - #[automatically_derived] - impl#ext_impl_generics ::juniper::marker::GraphQLUnion<#scalar> for #ty_full - #where_clause - { - fn mark() { - #all_variants_unique + /// Returns generated code for the [`GraphQLValue::resolve_into_type`][0] + /// method, which resolves the underlying GraphQL type contained in this + /// [`VariantDefinition`] synchronously. + /// + /// [0]: juniper::GraphQLValue::resolve_into_type + #[must_use] + fn method_resolve_into_type_tokens(&self, scalar: &scalar::Type) -> TokenStream { + let ty = &self.ty; + let expr = &self.resolver_code; + let resolving_code = gen::sync_resolving_code(); - #( <#var_types as ::juniper::marker::GraphQLObjectType<#scalar>>::mark(); )* - } + quote! { + if type_name == <#ty as ::juniper::GraphQLType<#scalar>>::name(info).unwrap() { + let res = { #expr }; + return #resolving_code; } - }; + } + } - into.append_all(&[ - union_impl, - output_type_impl, - type_impl, - value_impl, - value_async_impl, - ]); + /// Returns generated code for the + /// [`GraphQLValueAsync::resolve_into_type_async`][0] method, which + /// resolves the underlying GraphQL type contained in this + /// [`VariantDefinition`] asynchronously. + /// + /// [0]: juniper::GraphQLValueAsync::resolve_into_type_async + #[must_use] + fn method_resolve_into_type_async_tokens(&self, scalar: &scalar::Type) -> TokenStream { + let ty = &self.ty; + let expr = &self.resolver_code; + let resolving_code = gen::async_resolving_code(None); + + quote! { + if type_name == <#ty as ::juniper::GraphQLType<#scalar>>::name(info).unwrap() { + let fut = ::juniper::futures::future::ready({ #expr }); + return #resolving_code; + } + } } } -/// Emerges [`UnionMeta::external_resolvers`] into the given [GraphQL union][1] `variants`. +/// Emerges [`Attr::external_resolvers`] into the given [GraphQL union][1] +/// `variants`. /// /// If duplication happens, then resolving code is overwritten with the one from /// `external_resolvers`. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions -fn emerge_union_variants_from_meta( - variants: &mut Vec, - external_resolvers: UnionMetaResolvers, +fn emerge_union_variants_from_attr( + variants: &mut Vec, + external_resolvers: AttrResolvers, ) { if external_resolvers.is_empty() { return; @@ -597,9 +722,10 @@ fn emerge_union_variants_from_meta( let resolver_code = parse_quote! { #resolver_fn(self, ::juniper::FromContext::from(context)) }; - // Doing this may be quite an expensive, because resolving may contain some heavy - // computation, so we're preforming it twice. Unfortunately, we have no other options here, - // until the `juniper::GraphQLType` itself will allow to do it in some cleverer way. + // Doing this may be quite an expensive, because resolving may contain + // some heavy computation, so we're preforming it twice. Unfortunately, + // we have no other options here, until the `juniper::GraphQLType` + // itself will allow to do it in some cleverer way. let resolver_check = parse_quote! { ({ #resolver_code } as ::std::option::Option<&#ty>).is_some() }; @@ -608,29 +734,30 @@ fn emerge_union_variants_from_meta( var.resolver_code = resolver_code; var.resolver_check = resolver_check; } else { - variants.push(UnionVariantDefinition { + variants.push(VariantDefinition { ty, resolver_code, resolver_check, - context_ty: None, + context: None, }) } } } -/// Checks whether all [GraphQL union][1] `variants` represent a different Rust type. +/// Checks whether all [GraphQL union][1] `variants` represent a different Rust +/// type. /// /// # Notice /// -/// This is not an optimal implementation, as it's possible to bypass this check by using a full -/// qualified path instead (`crate::Test` vs `Test`). Since this requirement is mandatory, the -/// static assertion [`assert_type_ne_all!`][2] is used to enforce this requirement in the generated -/// code. However, due to the bad error message this implementation should stay and provide -/// guidance. +/// This is not an optimal implementation, as it's possible to bypass this check +/// by using a full qualified path instead (`crate::Test` vs `Test`). Since this +/// requirement is mandatory, the static assertion [`assert_type_ne_all!`][2] is +/// used to enforce this requirement in the generated code. However, due to the +/// bad error message this implementation should stay and provide guidance. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions -/// [2]: https://docs.rs/static_assertions/latest/static_assertions/macro.assert_type_ne_all.html -fn all_variants_different(variants: &[UnionVariantDefinition]) -> bool { +/// [2]: juniper::sa::assert_type_ne_all +fn all_variants_different(variants: &[VariantDefinition]) -> bool { let mut types: Vec<_> = variants.iter().map(|var| &var.ty).collect(); types.dedup(); types.len() == variants.len() diff --git a/juniper_codegen/src/impl_object.rs b/juniper_codegen/src/impl_object.rs deleted file mode 100644 index 9de0a7551..000000000 --- a/juniper_codegen/src/impl_object.rs +++ /dev/null @@ -1,231 +0,0 @@ -#![allow(clippy::collapsible_if)] - -use crate::{ - result::{GraphQLScope, UnsupportedAttribute}, - util::{self, span_container::SpanContainer, RenameRule}, -}; -use proc_macro2::TokenStream; -use quote::quote; -use syn::{ext::IdentExt, spanned::Spanned}; - -/// Generate code for the juniper::graphql_object macro. -pub fn build_object(args: TokenStream, body: TokenStream, error: GraphQLScope) -> TokenStream { - let definition = match create(args, body, error) { - Ok(definition) => definition, - Err(err) => return err.to_compile_error(), - }; - definition.into_tokens() -} - -/// Generate code for the juniper::graphql_subscription macro. -pub fn build_subscription( - args: TokenStream, - body: TokenStream, - error: GraphQLScope, -) -> TokenStream { - let definition = match create(args, body, error) { - Ok(definition) => definition, - Err(err) => return err.to_compile_error(), - }; - definition.into_subscription_tokens() -} - -fn create( - args: TokenStream, - body: TokenStream, - error: GraphQLScope, -) -> syn::Result { - let body_span = body.span(); - let _impl = util::parse_impl::ImplBlock::parse(args, body)?; - let name = _impl - .attrs - .name - .clone() - .map(SpanContainer::into_inner) - .unwrap_or_else(|| _impl.type_ident.unraw().to_string()); - - let top_attrs = &_impl.attrs; - - let scalar = _impl - .attrs - .scalar - .as_ref() - .map(|s| quote!( #s )) - .unwrap_or_else(|| quote!(::juniper::DefaultScalarValue)); - - let fields = _impl - .methods - .iter() - .filter_map(|method| { - let span = method.span(); - let _type = match method.sig.output { - syn::ReturnType::Type(_, ref t) => *t.clone(), - syn::ReturnType::Default => { - error.emit_custom(method.sig.span(), "return value required"); - return None; - } - }; - - let is_async = method.sig.asyncness.is_some(); - - let attrs = match util::FieldAttributes::from_attrs( - &method.attrs, - util::FieldAttributeParseMode::Impl, - ) { - Ok(attrs) => attrs, - Err(err) => { - proc_macro_error::emit_error!(err); - return None; - } - }; - - let parse_method = - _impl.parse_method(&method, true, |captured, arg_ident, is_mut: bool| { - let arg_name = arg_ident.unraw().to_string(); - let ty = &captured.ty; - - let final_name = attrs - .argument(&arg_name) - .and_then(|attrs| attrs.rename.clone().map(|ident| ident.value())) - .unwrap_or_else(|| { - top_attrs - .rename - .unwrap_or(RenameRule::CamelCase) - .apply(&arg_name) - }); - - let mut_modifier = if is_mut { quote!(mut) } else { quote!() }; - - if final_name.starts_with("__") { - error.no_double_underscore( - if let Some(name) = attrs - .argument(&arg_name) - .and_then(|attrs| attrs.rename.as_ref()) - { - name.span_ident() - } else { - arg_ident.span() - }, - ); - } - - let resolver = quote!( - let #mut_modifier #arg_ident = args - .get::<#ty>(#final_name) - .unwrap_or_else(::juniper::FromInputValue::<#scalar>::from_implicit_null); - ); - - let field_type = util::GraphQLTypeDefinitionFieldArg { - description: attrs - .argument(&arg_name) - .and_then(|arg| arg.description.as_ref().map(|d| d.value())), - default: attrs - .argument(&arg_name) - .and_then(|arg| arg.default.clone()), - _type: ty.clone(), - name: final_name, - }; - Ok((resolver, field_type)) - }); - - let (resolve_parts, args) = match parse_method { - Ok((resolve_parts, args)) => (resolve_parts, args), - Err(err) => { - proc_macro_error::emit_error!(err); - return None; - } - }; - - let body = &method.block; - let resolver_code = quote!( - #( #resolve_parts )* - #body - ); - - let ident = &method.sig.ident; - let name = attrs - .name - .clone() - .map(SpanContainer::into_inner) - .unwrap_or_else(|| { - top_attrs - .rename - .unwrap_or(RenameRule::CamelCase) - .apply(&ident.unraw().to_string()) - }); - - if name.starts_with("__") { - error.no_double_underscore(if let Some(name) = attrs.name { - name.span_ident() - } else { - ident.span() - }); - } - - if let Some(default) = attrs.default { - error.unsupported_attribute_within( - default.span_ident(), - UnsupportedAttribute::Default, - ); - } - - Some(util::GraphQLTypeDefinitionField { - name, - _type, - args, - description: attrs.description.map(SpanContainer::into_inner), - deprecation: attrs.deprecation.map(SpanContainer::into_inner), - resolver_code, - is_type_inferred: false, - is_async, - default: None, - span, - }) - }) - .collect::>(); - - // Early abort after checking all fields - proc_macro_error::abort_if_dirty(); - - if let Some(duplicates) = - crate::util::duplicate::Duplicate::find_by_key(&fields, |field| &field.name) - { - error.duplicate(duplicates.iter()) - } - - if !_impl.attrs.is_internal && name.starts_with("__") { - error.no_double_underscore(if let Some(name) = _impl.attrs.name { - name.span_ident() - } else { - _impl.type_ident.span() - }); - } - - if fields.is_empty() { - error.not_empty(body_span); - } - - // Early abort after GraphQL properties - proc_macro_error::abort_if_dirty(); - - let definition = util::GraphQLTypeDefiniton { - name, - _type: *_impl.target_type.clone(), - scalar: _impl.attrs.scalar.map(SpanContainer::into_inner), - context: _impl.attrs.context.map(SpanContainer::into_inner), - description: _impl.description, - fields, - generics: _impl.generics.clone(), - interfaces: _impl - .attrs - .interfaces - .into_iter() - .map(SpanContainer::into_inner) - .collect(), - include_type_generics: false, - generic_scalar: true, - no_async: _impl.attrs.no_async.is_some(), - }; - - Ok(definition) -} diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 38870ff28..e0b0729c9 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -108,13 +108,13 @@ macro_rules! try_merge_hashset { mod derive_enum; mod derive_input_object; -mod derive_object; mod derive_scalar_value; -mod impl_object; mod impl_scalar; mod common; mod graphql_interface; +mod graphql_object; +mod graphql_subscription; mod graphql_union; use proc_macro::TokenStream; @@ -143,17 +143,6 @@ pub fn derive_input_object(input: TokenStream) -> TokenStream { } } -#[proc_macro_error] -#[proc_macro_derive(GraphQLObject, attributes(graphql))] -pub fn derive_object(input: TokenStream) -> TokenStream { - let ast = syn::parse::(input).unwrap(); - let gen = derive_object::build_derive_object(ast, GraphQLScope::DeriveObject); - match gen { - Ok(gen) => gen.into(), - Err(err) => proc_macro_error::abort!(err), - } -} - /// This custom derive macro implements the #[derive(GraphQLScalarValue)] /// derive. /// @@ -207,273 +196,6 @@ pub fn derive_scalar_value(input: TokenStream) -> TokenStream { } } -/** -The `object` proc macro is the primary way of defining GraphQL resolvers -that can not be implemented with the GraphQLObject derive. - -It enables you to write GraphQL field resolvers for a type by declaring a -regular Rust `impl` block. Under the hood, the procedural macro implements -the GraphQLType trait. - -`object` comes with many features that allow customization of -your fields, all of which are detailed below. - -### Getting Started - -This simple example will show you the most basic use of `object`. -More advanced use cases are introduced step by step. - -``` -// So we can declare it as a plain struct without any members. -struct Query; - -// We prefix the impl Block with the procedural macro. -#[juniper::graphql_object] -impl Query { - - // A **warning**: only GraphQL fields can be specified in this impl block. - // If you want to define normal methods on the struct, - // you have to do so in a separate, normal `impl` block. - - - // This defines a simple, static field which does not require any context. - // You can return any value that implements the `GraphQLType` trait. - // This trait is implemented for: - // - basic scalar types like bool, &str, String, i32, f64 - // - GraphQL compatible wrappers like Option<_>, Vec<_>. - // - types which use the `#derive[juniper::GraphQLObject]` - // - `object` structs. - // - // An important note regarding naming: - // By default, field names will be converted to camel case. - // For your GraphQL queries, the field will be available as `apiVersion`. - // - // You can also manually customize the field name if required. (See below) - fn api_version() -> &'static str { - "0.1" - } - - // This field takes two arguments. - // GraphQL arguments are just regular function parameters. - // **Note**: in Juniper, arguments are non-nullable by default. - // for optional arguments, you have to specify them with Option. - fn add(a: f64, b: f64, c: Option) -> f64 { - a + b + c.unwrap_or(0.0) - } -} -``` - -## Accessing self - -``` -struct Person { - first_name: String, - last_name: String, -} - -impl Person { - // The full name method is useful outside of GraphQL, - // so we define it as a normal method. - fn build_full_name(&self) -> String { - format!("{} {}", self.first_name, self.last_name) - } -} - -#[juniper::graphql_object] -impl Person { - fn first_name(&self) -> &str { - &self.first_name - } - - fn last_name(&self) -> &str { - &self.last_name - } - - fn full_name(&self) -> String { - self.build_full_name() - } -} -``` - -## Context (+ Executor) - -You can specify a context that will be available across -all your resolvers during query execution. - -The Context can be injected into your resolvers by just -specifying an argument with the same type as the context -(but as a reference). - -``` - -# #[derive(juniper::GraphQLObject)] struct User { id: i32 } -# struct DbPool; -# impl DbPool { fn user(&self, id: i32) -> Option { unimplemented!() } } - -struct Context { - db: DbPool, -} - -// Mark our struct for juniper. -impl juniper::Context for Context {} - -struct Query; - -#[juniper::graphql_object( - // Here we specify the context type for this object. - Context = Context, -)] -impl Query { - // Context is injected by specifying a argument - // as a reference to the Context. - fn user(context: &Context, id: i32) -> Option { - context.db.user(id) - } - - // You can also gain access to the executor, which - // allows you to do look aheads. - fn with_executor(executor: &Executor) -> bool { - let info = executor.look_ahead(); - // ... - true - } -} - -``` - -## Customization (Documentation, Renaming, ...) - -``` -struct InternalQuery; - -// Doc comments can be used to specify graphql documentation. -/// GRAPHQL DOCUMENTATION. -/// More info for GraphQL users.... -#[juniper::graphql_object( - // You can rename the type for GraphQL by specifying the name here. - name = "Query", - // You can also specify a description here. - // If present, doc comments will be ignored. - description = "...", -)] -impl InternalQuery { - // Documentation doc comments also work on fields. - /// GraphQL description... - fn field_with_description() -> bool { true } - - // Fields can also be customized with the #[graphql] attribute. - #[graphql( - // overwrite the public name - name = "actualFieldName", - // Can be used instead of doc comments. - description = "field description", - )] - fn internal_name() -> bool { true } - - // Fields can be deprecated too. - #[graphql( - deprecated = "deprecatin info...", - // Note: just "deprecated," without a description works too. - )] - fn deprecated_field_simple() -> bool { true } - - - // Customizing field arguments is a little awkward right now. - // This will improve once [RFC 2564](https://github.com/rust-lang/rust/issues/60406) - // is implemented, which will allow attributes on function parameters. - - #[graphql( - arguments( - arg1( - // You can specify default values. - // A default can be any valid expression that yields the right type. - default = true, - description = "Argument description....", - ), - arg2( - default = false, - description = "arg2 description...", - ), - ), - )] - fn args(arg1: bool, arg2: bool) -> bool { - arg1 && arg2 - } -} -``` - -## Lifetimes, Generics and custom Scalars - -Lifetimes work just like you'd expect. - - -``` -struct WithLifetime<'a> { - value: &'a str, -} - -#[juniper::graphql_object] -impl<'a> WithLifetime<'a> { - fn value(&self) -> &str { - self.value - } -} - -``` - -Juniper has support for custom scalars. -Mostly you will only need the default scalar type juniper::DefaultScalarValue. - -You can easily specify a custom scalar though. - - -``` - -# type MyCustomScalar = juniper::DefaultScalarValue; - -struct Query; - -#[juniper::graphql_object( - Scalar = MyCustomScalar, -)] -impl Query { - fn test(&self) -> i32 { - 0 - } -} -``` - -## Raw identifiers - -You can use [raw identifiers](https://doc.rust-lang.org/stable/edition-guide/rust-2018/module-system/raw-identifiers.html) -if you want a GrahpQL field that happens to be a Rust keyword: - -``` -struct User { - r#type: String, -} - -#[juniper::graphql_object] -impl User { - fn r#type(&self) -> &str { - &self.r#type - } -} -``` - -*/ -#[proc_macro_error] -#[proc_macro_attribute] -pub fn graphql_object(args: TokenStream, input: TokenStream) -> TokenStream { - let args = proc_macro2::TokenStream::from(args); - let input = proc_macro2::TokenStream::from(input); - TokenStream::from(impl_object::build_object( - args, - input, - GraphQLScope::ImplObject, - )) -} - /// Expose GraphQL scalars /// /// The GraphQL language defines a number of built-in scalars: strings, numbers, and @@ -495,7 +217,7 @@ pub fn graphql_object(args: TokenStream, input: TokenStream) -> TokenStream { /// struct UserID(String); /// /// #[juniper::graphql_scalar( -/// // You can rename the type for GraphQL by specifying the name here. +/// // You can rename the type for GraphQL by specifying the name here. /// name = "MyName", /// // You can also specify a description here. /// // If present, doc comments will be ignored. @@ -535,36 +257,26 @@ pub fn graphql_scalar(args: TokenStream, input: TokenStream) -> TokenStream { } } -/// A proc macro for defining a GraphQL subscription. -#[proc_macro_error] -#[proc_macro_attribute] -pub fn graphql_subscription(args: TokenStream, input: TokenStream) -> TokenStream { - let args = proc_macro2::TokenStream::from(args); - let input = proc_macro2::TokenStream::from(input); - TokenStream::from(impl_object::build_subscription( - args, - input, - GraphQLScope::ImplObject, - )) -} - -/// `#[graphql_interface]` macro for generating a [GraphQL interface][1] implementation for traits -/// and its implementers. +/// `#[graphql_interface]` macro for generating a [GraphQL interface][1] +/// implementation for traits and its implementers. /// -/// Specifying multiple `#[graphql_interface]` attributes on the same definition is totally okay. -/// They all will be treated as a single attribute. +/// Specifying multiple `#[graphql_interface]` attributes on the same definition +/// is totally okay. They all will be treated as a single attribute. /// -/// The main difference between [GraphQL interface][1] type and Rust trait is that the former serves -/// both as an _abstraction_ and a _value downcastable to concrete implementers_, while in Rust, a -/// trait is an _abstraction only_ and you need a separate type to downcast into a concrete -/// implementer, like enum or [trait object][3], because trait doesn't represent a type itself. -/// Macro uses Rust enum to represent a value type of [GraphQL interface][1] by default, however -/// [trait object][3] may be used too (use `dyn` attribute argument for that). +/// The main difference between [GraphQL interface][1] type and Rust trait is +/// that the former serves both as an _abstraction_ and a _value downcastable to +/// concrete implementers_, while in Rust, a trait is an _abstraction only_ and +/// you need a separate type to downcast into a concrete implementer, like enum +/// or [trait object][3], because trait doesn't represent a type itself. +/// Macro uses Rust enum to represent a value type of [GraphQL interface][1] by +/// default, however [trait object][3] may be used too (use `dyn` attribute +/// argument for that). /// -/// A __trait has to be [object safe][2]__ if its values are represented by [trait object][3], -/// because schema resolvers will need to return that [trait object][3]. The [trait object][3] has -/// to be [`Send`] and [`Sync`], and the macro automatically generate a convenien type alias for -/// such [trait object][3]. +/// A __trait has to be [object safe][2]__ if its values are represented by +/// [trait object][3], because schema resolvers will need to return that +/// [trait object][3]. The [trait object][3] has to be [`Send`] and [`Sync`], +/// and the macro automatically generate a convenien type alias for such +/// [trait object][3]. /// /// ``` /// use juniper::{graphql_interface, GraphQLObject}; @@ -619,7 +331,7 @@ pub fn graphql_subscription(args: TokenStream, input: TokenStream) -> TokenStrea /// /// # Custom name, description, deprecation and argument defaults /// -/// The name of [GraphQL interface][1], its field, or a field argument may be overriden with a +/// The name of [GraphQL interface][1], its field, or a field argument may be overridden with a /// `name` attribute's argument. By default, a type name is used or `camelCased` method/argument /// name. /// @@ -627,13 +339,12 @@ pub fn graphql_subscription(args: TokenStream, input: TokenStream) -> TokenStrea /// either with a `description`/`desc` attribute's argument, or with a regular Rust doc comment. /// /// A field of [GraphQL interface][1] may be deprecated by specifying a `deprecated` attribute's -/// argument, or with regulat Rust `#[deprecated]` attribute. +/// argument, or with regular Rust `#[deprecated]` attribute. /// /// The default value of a field argument may be specified with a `default` attribute argument (if /// no exact value is specified then [`Default::default`] is used). /// /// ``` -/// # #![allow(deprecated)] /// # use juniper::graphql_interface; /// # /// #[graphql_interface(name = "Character", desc = "Possible episode characters.")] @@ -658,16 +369,72 @@ pub fn graphql_subscription(args: TokenStream, input: TokenStream) -> TokenStrea /// } /// ``` /// +/// # Renaming policy +/// +/// By default, all [GraphQL interface][1] fields and their arguments are renamed +/// via `camelCase` policy (so `fn my_id(&self) -> String` becomes `myId` field +/// in GraphQL schema, and so on). This complies with default GraphQL naming +/// conventions [demonstrated in spec][0]. +/// +/// 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`, +/// `none` (disables any renaming). +/// +/// ``` +/// # use juniper::{graphql_interface, GraphQLObject}; +/// # +/// #[graphql_interface(for = Human, rename_all = "none")] // disables renaming +/// trait Character { +/// // NOTICE: In the generated GraphQL schema this field and its argument +/// // will be `detailed_info` and `info_kind`. +/// fn detailed_info(&self, info_kind: String) -> String; +/// } +/// +/// #[derive(GraphQLObject)] +/// #[graphql(impl = CharacterValue)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// #[graphql_interface] +/// impl Character for Human { +/// fn detailed_info(&self, info_kind: String) -> String { +/// (info_kind == "planet") +/// .then(|| &self.home_planet) +/// .unwrap_or(&self.id) +/// .clone() +/// } +/// } +/// ``` +/// +/// # Ignoring trait methods +/// +/// To omit some trait method to be assumed as a [GraphQL interface][1] field +/// and ignore it, use an `ignore` attribute's argument directly on that method. +/// +/// ``` +/// # use juniper::graphql_interface; +/// # +/// #[graphql_interface] +/// trait Character { +/// fn id(&self) -> &str; +/// +/// #[graphql(ignore)] +/// fn kaboom(&mut self); +/// } +/// ``` +/// /// # Custom context /// /// By default, the generated implementation tries to infer [`Context`] type from signatures of /// trait methods, and uses [unit type `()`][4] if signatures contains no [`Context`] arguments. /// /// If [`Context`] type cannot be inferred or is inferred incorrectly, then specify it explicitly -/// with `context`/`Context` attribute's argument. +/// with `context` attribute's argument. /// /// If trait method represents a [GraphQL interface][1] field and its argument is named as `context` -/// or `ctx` then this argument is assumed as [`Context`] and will be omited in GraphQL schema. +/// or `ctx` then this argument is assumed as [`Context`] and will be omitted in GraphQL schema. /// Additionally, any argument may be marked as [`Context`] with a `context` attribute's argument. /// /// ``` @@ -723,7 +490,7 @@ pub fn graphql_subscription(args: TokenStream, input: TokenStream) -> TokenStrea /// /// If an [`Executor`] is required in a trait method to resolve a [GraphQL interface][1] field, /// specify it as an argument named as `executor` or explicitly marked with an `executor` -/// attribute's argument. Such method argument will be omited in GraphQL schema. +/// attribute's argument. Such method argument will be omitted in GraphQL schema. /// /// However, this requires to explicitly parametrize over [`ScalarValue`], as [`Executor`] does so. /// @@ -731,7 +498,7 @@ pub fn graphql_subscription(args: TokenStream, input: TokenStream) -> TokenStrea /// # use juniper::{graphql_interface, Executor, GraphQLObject, LookAheadMethods as _, ScalarValue}; /// # /// // NOTICE: Specifying `ScalarValue` as existing type parameter. -/// #[graphql_interface(for = Human, Scalar = S)] +/// #[graphql_interface(for = Human, scalar = S)] /// trait Character { /// async fn id<'a>(&self, executor: &'a Executor<'_, '_, (), S>) -> &'a str /// where @@ -751,7 +518,7 @@ pub fn graphql_subscription(args: TokenStream, input: TokenStream) -> TokenStrea /// id: String, /// name: String, /// } -/// #[graphql_interface(Scalar = S)] +/// #[graphql_interface(scalar = S)] /// impl Character for Human { /// async fn id<'a>(&self, executor: &'a Executor<'_, '_, (), S>) -> &'a str /// where @@ -771,28 +538,29 @@ pub fn graphql_subscription(args: TokenStream, input: TokenStream) -> TokenStrea /// /// # Custom `ScalarValue` /// -/// By default, `#[graphql_interface]` macro generates code, which is generic over a [`ScalarValue`] -/// type. This may introduce a problem when at least one of [GraphQL interface][1] implementers is -/// restricted to a concrete [`ScalarValue`] type in its implementation. To resolve such problem, a -/// concrete [`ScalarValue`] type should be specified with a `scalar`/`Scalar`/`ScalarValue` +/// By default, `#[graphql_interface]` macro generates code, which is generic +/// over a [`ScalarValue`] type. This may introduce a problem when at least one +/// of [GraphQL interface][1] implementers is restricted to a concrete +/// [`ScalarValue`] type in its implementation. To resolve such problem, a +/// concrete [`ScalarValue`] type should be specified with a `scalar` /// attribute's argument. /// /// ``` /// # use juniper::{graphql_interface, DefaultScalarValue, GraphQLObject}; /// # /// // NOTICE: Removing `Scalar` argument will fail compilation. -/// #[graphql_interface(for = [Human, Droid], Scalar = DefaultScalarValue)] +/// #[graphql_interface(for = [Human, Droid], scalar = DefaultScalarValue)] /// trait Character { /// fn id(&self) -> &str; /// } /// /// #[derive(GraphQLObject)] -/// #[graphql(impl = CharacterValue, Scalar = DefaultScalarValue)] +/// #[graphql(impl = CharacterValue, scalar = DefaultScalarValue)] /// struct Human { /// id: String, /// home_planet: String, /// } -/// #[graphql_interface(Scalar = DefaultScalarValue)] +/// #[graphql_interface(scalar = DefaultScalarValue)] /// impl Character for Human { /// fn id(&self) -> &str{ /// &self.id @@ -800,12 +568,12 @@ pub fn graphql_subscription(args: TokenStream, input: TokenStream) -> TokenStrea /// } /// /// #[derive(GraphQLObject)] -/// #[graphql(impl = CharacterValue, Scalar = DefaultScalarValue)] +/// #[graphql(impl = CharacterValue, scalar = DefaultScalarValue)] /// struct Droid { /// id: String, /// primary_function: String, /// } -/// #[graphql_interface(Scalar = DefaultScalarValue)] +/// #[graphql_interface(scalar = DefaultScalarValue)] /// impl Character for Droid { /// fn id(&self) -> &str { /// &self.id @@ -813,23 +581,6 @@ pub fn graphql_subscription(args: TokenStream, input: TokenStream) -> TokenStrea /// } /// ``` /// -/// # Ignoring trait methods -/// -/// To omit some trait method to be assumed as a [GraphQL interface][1] field and ignore it, use an -/// `ignore`/`skip` attribute's argument directly on that method. -/// -/// ``` -/// # use juniper::graphql_interface; -/// # -/// #[graphql_interface] -/// trait Character { -/// fn id(&self) -> &str; -/// -/// #[graphql(ignore)] // or `#[graphql(skip)]`, your choice -/// fn kaboom(&mut self); -/// } -/// ``` -/// /// # Downcasting /// /// By default, the [GraphQL interface][1] value is downcast to one of its implementer types via @@ -901,6 +652,7 @@ pub fn graphql_subscription(args: TokenStream, input: TokenStream) -> TokenStrea /// [`Context`]: juniper::Context /// [`Executor`]: juniper::Executor /// [`ScalarValue`]: juniper::ScalarValue +/// [0]: https://spec.graphql.org/June2018 /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces /// [2]: https://doc.rust-lang.org/stable/reference/items/traits.html#object-safety /// [3]: https://doc.rust-lang.org/stable/reference/types/trait-object.html @@ -913,6 +665,521 @@ pub fn graphql_interface(attr: TokenStream, body: TokenStream) -> TokenStream { .into() } +/// `#[derive(GraphQLObject)]` macro for deriving a [GraphQL object][1] +/// implementation for structs. +/// +/// 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. +/// +/// ``` +/// use juniper::GraphQLObject; +/// +/// #[derive(GraphQLObject)] +/// struct Query { +/// // NOTICE: By default, field names will be converted to `camelCase`. +/// // In the generated GraphQL schema this field will be available +/// // as `apiVersion`. +/// api_version: &'static str, +/// } +/// ``` +/// +/// # Custom name, description and deprecation +/// +/// The name of [GraphQL object][1] or its field may be overridden with a `name` +/// attribute's argument. By default, a type name is used or `camelCased` field +/// name. +/// +/// The description of [GraphQL object][1] or its field may be specified either +/// with a `description`/`desc` attribute's argument, or with a regular Rust doc +/// comment. +/// +/// A field of [GraphQL object][1] may be deprecated by specifying a +/// `deprecated` attribute's argument, or with regular Rust `#[deprecated]` +/// attribute. +/// +/// ``` +/// # use juniper::GraphQLObject; +/// # +/// #[derive(GraphQLObject)] +/// #[graphql( +/// // Rename the type for GraphQL by specifying the name here. +/// name = "Human", +/// // You may also specify a description here. +/// // If present, doc comments will be ignored. +/// desc = "Possible episode human.", +/// )] +/// struct HumanWithAttrs { +/// #[graphql(name = "id", desc = "ID of the human.")] +/// #[graphql(deprecated = "Don't use it")] +/// some_id: String, +/// } +/// +/// // Rust docs are used as GraphQL description. +/// /// Possible episode human. +/// #[derive(GraphQLObject)] +/// struct HumanWithDocs { +/// // Doc comments also work on fields. +/// /// ID of the human. +/// #[deprecated] +/// id: String, +/// } +/// ``` +/// +/// # Renaming policy +/// +/// By default, all [GraphQL object][1] fields are renamed via `camelCase` +/// policy (so `api_version: String` becomes `apiVersion` field in GraphQL +/// schema, and so on). This complies with default GraphQL naming conventions +/// [demonstrated in spec][0]. +/// +/// 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`, +/// `none` (disables any renaming). +/// +/// ``` +/// # use juniper::GraphQLObject; +/// # +/// #[derive(GraphQLObject)] +/// #[graphql(rename_all = "none")] // disables renaming +/// struct Query { +/// // NOTICE: In the generated GraphQL schema this field will be available +/// // as `api_version`. +/// api_version: String, +/// } +/// ``` +/// +/// # Ignoring struct fields +/// +/// To omit exposing a struct field in the GraphQL schema, use an `ignore` +/// attribute's argument directly on that field. +/// +/// ``` +/// # use juniper::GraphQLObject; +/// # +/// #[derive(GraphQLObject)] +/// struct Human { +/// id: String, +/// #[graphql(ignore)] +/// home_planet: String, +/// } +/// ``` +/// +/// # Custom `ScalarValue` +/// +/// By default, `#[derive(GraphQLObject)]` macro generates code, which is +/// generic over a [`ScalarValue`] type. This may introduce a problem when at +/// least one of its fields is restricted to a concrete [`ScalarValue`] type in +/// its implementation. To resolve such problem, a concrete [`ScalarValue`] type +/// should be specified with a `scalar` attribute's argument. +/// +/// ``` +/// # use juniper::{DefaultScalarValue, GraphQLObject}; +/// # +/// #[derive(GraphQLObject)] +/// // NOTICE: Removing `scalar` argument will fail compilation. +/// #[graphql(scalar = DefaultScalarValue)] +/// struct Human { +/// id: String, +/// helper: Droid, +/// } +/// +/// #[derive(GraphQLObject)] +/// #[graphql(scalar = DefaultScalarValue)] +/// struct Droid { +/// id: String, +/// } +/// ``` +/// +/// [`ScalarValue`]: juniper::ScalarValue +/// [1]: https://spec.graphql.org/June2018/#sec-Objects +#[proc_macro_error] +#[proc_macro_derive(GraphQLObject, attributes(graphql))] +pub fn derive_object(body: TokenStream) -> TokenStream { + self::graphql_object::derive::expand(body.into()) + .unwrap_or_abort() + .into() +} + +/// `#[graphql_object]` macro for generating a [GraphQL object][1] +/// implementation for structs with computable field resolvers (declared via +/// a regular Rust `impl` block). +/// +/// It enables you to write GraphQL field resolvers for a type by declaring a +/// regular Rust `impl` block. Under the hood, the macro implements +/// [`GraphQLType`]/[`GraphQLValue`] traits. +/// +/// Specifying multiple `#[graphql_object]` attributes on the same definition +/// is totally okay. They all will be treated as a single attribute. +/// +/// ``` +/// use juniper::graphql_object; +/// +/// // We can declare the type as a plain struct without any members. +/// struct Query; +/// +/// #[graphql_object] +/// impl Query { +/// // WARNING: Only GraphQL fields can be specified in this `impl` block. +/// // If normal methods are required on the struct, they can be +/// // defined either in a separate "normal" `impl` block, or +/// // marked with `#[graphql(ignore)]` attribute. +/// +/// // This defines a simple, static field which does not require any +/// // context. +/// // Such field can return any value that implements `GraphQLType` and +/// // `GraphQLValue` traits. +/// // +/// // NOTICE: By default, field names will be converted to `camelCase`. +/// // In the generated GraphQL schema this field will be available +/// // as `apiVersion`. +/// fn api_version() -> &'static str { +/// "0.1" +/// } +/// +/// // This field takes two arguments. +/// // GraphQL arguments are just regular function parameters. +/// // +/// // NOTICE: In `juniper`, arguments are non-nullable by default. For +/// // optional arguments, you have to specify them as `Option<_>`. +/// async fn add(a: f64, b: f64, c: Option) -> f64 { +/// a + b + c.unwrap_or(0.0) +/// } +/// } +/// ``` +/// +/// # Accessing self +/// +/// Fields may also have a `self` receiver. +/// +/// ``` +/// # use juniper::graphql_object; +/// # +/// struct Person { +/// first_name: String, +/// last_name: String, +/// } +/// +/// #[graphql_object] +/// impl Person { +/// fn first_name(&self) -> &str { +/// &self.first_name +/// } +/// +/// fn last_name(&self) -> &str { +/// &self.last_name +/// } +/// +/// fn full_name(&self) -> String { +/// self.build_full_name() +/// } +/// +/// // This method is useful only to define GraphQL fields, but is not +/// // a field itself, so we ignore it in schema. +/// #[graphql(ignore)] +/// fn build_full_name(&self) -> String { +/// format!("{} {}", self.first_name, self.last_name) +/// } +/// } +/// ``` +/// +/// # Custom name, description, deprecation and argument defaults +/// +/// The name of [GraphQL object][1], its field, or a field argument may be +/// overridden with a `name` attribute's argument. By default, a type name is +/// used or `camelCased` method/argument name. +/// +/// The description of [GraphQL object][1], its field, or a field argument may +/// be specified either with a `description`/`desc` attribute's argument, or +/// with a regular Rust doc comment. +/// +/// A field of [GraphQL object][1] may be deprecated by specifying a +/// `deprecated` attribute's argument, or with regular Rust `#[deprecated]` +/// attribute. +/// +/// The default value of a field argument may be specified with a `default` +/// attribute argument (if no exact value is specified then [`Default::default`] +/// is used). +/// +/// ``` +/// # use juniper::graphql_object; +/// # +/// struct HumanWithAttrs; +/// +/// #[graphql_object( +/// // Rename the type for GraphQL by specifying the name here. +/// name = "Human", +/// // You may also specify a description here. +/// // If present, doc comments will be ignored. +/// desc = "Possible episode human.", +/// )] +/// impl HumanWithAttrs { +/// #[graphql(name = "id", desc = "ID of the human.")] +/// #[graphql(deprecated = "Don't use it")] +/// fn some_id( +/// &self, +/// #[graphql(name = "number", desc = "Arbitrary number.")] +/// // You may specify default values. +/// // A default can be any valid expression that yields the right type. +/// #[graphql(default = 5)] +/// num: i32, +/// ) -> &str { +/// "Don't use me!" +/// } +/// } +/// +/// struct HumanWithDocs; +/// +/// // Rust docs are used as GraphQL description. +/// /// Possible episode human. +/// #[graphql_object] +/// impl HumanWithDocs { +/// // Doc comments also work on fields. +/// /// ID of the human. +/// #[deprecated] +/// fn id( +/// &self, +/// // If expression is not specified then `Default::default()` is used. +/// #[graphql(default)] num: i32, +/// ) -> &str { +/// "Deprecated" +/// } +/// } +/// ``` +/// +/// # Renaming policy +/// +/// By default, all [GraphQL object][1] fields and their arguments are renamed +/// via `camelCase` policy (so `fn api_version() -> String` becomes `apiVersion` +/// field in GraphQL schema, and so on). This complies with default GraphQL +/// naming conventions [demonstrated in spec][0]. +/// +/// 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`, +/// `none` (disables any renaming). +/// +/// ``` +/// # use juniper::graphql_object; +/// # +/// struct Query; +/// +/// #[graphql_object(rename_all = "none")] // disables renaming +/// impl Query { +/// // NOTICE: In the generated GraphQL schema this field will be available +/// // as `api_version`. +/// fn api_version() -> &'static str { +/// "0.1" +/// } +/// +/// // NOTICE: In the generated GraphQL schema these field arguments will be +/// // available as `arg_a` and `arg_b`. +/// async fn add(arg_a: f64, arg_b: f64, c: Option) -> f64 { +/// arg_a + arg_b + c.unwrap_or(0.0) +/// } +/// } +/// ``` +/// +/// # Ignoring methods +/// +/// To omit some method to be assumed as a [GraphQL object][1] field and ignore +/// it, use an `ignore` attribute's argument directly on that method. +/// +/// ``` +/// # use juniper::graphql_object; +/// # +/// struct Human(String); +/// +/// #[graphql_object] +/// impl Human { +/// fn id(&self) -> &str { +/// &self.0 +/// } +/// +/// #[graphql(ignore)] +/// fn kaboom(&mut self) {} +/// } +/// ``` +/// +/// # Custom context +/// +/// By default, the generated implementation tries to infer [`Context`] type +/// from signatures of `impl` block methods, and uses [unit type `()`][4] if +/// signatures contains no [`Context`] arguments. +/// +/// If [`Context`] type cannot be inferred or is inferred incorrectly, then +/// specify it explicitly with `context` attribute's argument. +/// +/// If method argument is named as `context` or `ctx` then this argument is +/// assumed as [`Context`] and will be omitted in GraphQL schema. +/// Additionally, any argument may be marked as [`Context`] with a `context` +/// attribute's argument. +/// +/// ``` +/// # use std::collections::HashMap; +/// # use juniper::graphql_object; +/// # +/// struct Database { +/// humans: HashMap, +/// } +/// impl juniper::Context for Database {} +/// +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[graphql_object(context = Database)] +/// impl Human { +/// fn id<'db>(&self, context: &'db Database) -> Option<&'db str> { +/// context.humans.get(&self.id).map(|h| h.id.as_str()) +/// } +/// fn info<'db>(&self, context: &'db Database) -> Option<&'db str> { +/// context.humans.get(&self.id).map(|h| h.home_planet.as_str()) +/// } +/// } +/// ``` +/// +/// # Using `Executor` +/// +/// If an [`Executor`] is required in a method to resolve a [GraphQL object][1] +/// field, specify it as an argument named as `executor` or explicitly marked +/// with an `executor` attribute's argument. Such method argument will be +/// omitted in GraphQL schema. +/// +/// However, this requires to explicitly parametrize over [`ScalarValue`], as +/// [`Executor`] does so. +/// +/// ``` +/// # use juniper::{graphql_object, Executor, GraphQLObject, LookAheadMethods as _, ScalarValue}; +/// # +/// struct Human { +/// name: String, +/// } +/// +/// // NOTICE: Specifying `ScalarValue` as custom named type parameter. +/// // Its name should be similar to the one used in methods. +/// #[graphql_object(scalar = S: ScalarValue)] +/// impl Human { +/// async fn id<'a, S: ScalarValue>( +/// &self, +/// executor: &'a Executor<'_, '_, (), S>, +/// ) -> &'a str { +/// executor.look_ahead().field_name() +/// } +/// +/// fn name<'b, S: ScalarValue>( +/// &'b self, +/// #[graphql(executor)] _another: &Executor<'_, '_, (), S>, +/// ) -> &'b str { +/// &self.name +/// } +/// } +/// ``` +/// +/// # Custom `ScalarValue` +/// +/// By default, `#[graphql_object]` macro generates code, which is generic over +/// a [`ScalarValue`] type. This may introduce a problem when at least one of +/// its fields is restricted to a concrete [`ScalarValue`] type in its +/// implementation. To resolve such problem, a concrete [`ScalarValue`] type +/// should be specified with a `scalar` attribute's argument. +/// +/// ``` +/// # use juniper::{graphql_object, DefaultScalarValue, GraphQLObject}; +/// # +/// struct Human(String); +/// +/// // NOTICE: Removing `scalar` argument will fail compilation. +/// #[graphql_object(scalar = DefaultScalarValue)] +/// impl Human { +/// fn id(&self) -> &str { +/// &self.0 +/// } +/// +/// fn helper(&self) -> Droid { +/// Droid { +/// id: self.0.clone(), +/// } +/// } +/// } +/// +/// #[derive(GraphQLObject)] +/// #[graphql(scalar = DefaultScalarValue)] +/// struct Droid { +/// id: String, +/// } +/// ``` +/// +/// [`Context`]: juniper::Context +/// [`Executor`]: juniper::Executor +/// [`GraphQLType`]: juniper::GraphQLType +/// [`GraphQLValue`]: juniper::GraphQLValue +/// [`ScalarValue`]: juniper::ScalarValue +/// [0]: https://spec.graphql.org/June2018 +/// [1]: https://spec.graphql.org/June2018/#sec-Objects +#[proc_macro_error] +#[proc_macro_attribute] +pub fn graphql_object(attr: TokenStream, body: TokenStream) -> TokenStream { + self::graphql_object::attr::expand(attr.into(), body.into()) + .unwrap_or_abort() + .into() +} + +/// `#[graphql_subscription]` macro for generating a [GraphQL subscription][1] +/// implementation for structs with computable field resolvers (declared via +/// a regular Rust `impl` block). +/// +/// It enables you to write GraphQL field resolvers for a type by declaring a +/// regular Rust `impl` block. Under the hood, the macro implements +/// [`GraphQLType`]/[`GraphQLSubscriptionValue`] traits. +/// +/// Specifying multiple `#[graphql_subscription]` attributes on the same +/// definition is totally okay. They all will be treated as a single attribute. +/// +/// This macro is similar to [`#[graphql_object]` macro](macro@graphql_object) +/// and has all its properties, but requires methods to be `async` and return +/// [`Stream`] of values instead of a value itself. +/// +/// ``` +/// # use futures::stream::{self, BoxStream}; +/// use juniper::graphql_subscription; +/// +/// // We can declare the type as a plain struct without any members. +/// struct Subscription; +/// +/// #[graphql_subscription] +/// impl Subscription { +/// // WARNING: Only GraphQL fields can be specified in this `impl` block. +/// // If normal methods are required on the struct, they can be +/// // defined either in a separate "normal" `impl` block, or +/// // marked with `#[graphql(ignore)]` attribute. +/// +/// // This defines a simple, static field which does not require any +/// // context. +/// // Such field can return a `Stream` of any value implementing +/// // `GraphQLType` and `GraphQLValue` traits. +/// // +/// // NOTICE: Method must be `async`. +/// async fn api_version() -> BoxStream<'static, &'static str> { +/// Box::pin(stream::once(async { "0.1" })) +/// } +/// } +/// ``` +/// +/// [`GraphQLType`]: juniper::GraphQLType +/// [`GraphQLSubscriptionValue`]: juniper::GraphQLSubscriptionValue +/// [`Stream`]: futures::Stream +/// [1]: https://spec.graphql.org/June2018/#sec-Subscription +#[proc_macro_error] +#[proc_macro_attribute] +pub fn graphql_subscription(attr: TokenStream, body: TokenStream) -> TokenStream { + self::graphql_subscription::attr::expand(attr.into(), body.into()) + .unwrap_or_abort() + .into() +} + /// `#[derive(GraphQLUnion)]` macro for deriving a [GraphQL union][1] implementation for enums and /// structs. /// @@ -995,7 +1262,7 @@ pub fn graphql_interface(attr: TokenStream, body: TokenStream) -> TokenStream { /// /// By default, the generated implementation uses [unit type `()`][4] as [`Context`]. To use a /// custom [`Context`] type for [GraphQL union][1] variants types or external resolver functions, -/// specify it with `context`/`Context` attribute's argument. +/// specify it with `context` attribute's argument. /// /// ``` /// # use juniper::{GraphQLObject, GraphQLUnion}; @@ -1027,17 +1294,17 @@ pub fn graphql_interface(attr: TokenStream, body: TokenStream) -> TokenStream { /// /// # Custom `ScalarValue` /// -/// By default, this macro generates code, which is generic over a [`ScalarValue`] type. -/// This may introduce a problem when at least one of [GraphQL union][1] variants is restricted to a -/// concrete [`ScalarValue`] type in its implementation. To resolve such problem, a concrete -/// [`ScalarValue`] type should be specified with a `scalar`/`Scalar`/`ScalarValue` attribute's -/// argument. +/// By default, this macro generates code, which is generic over a +/// [`ScalarValue`] type. This may introduce a problem when at least one of +/// [GraphQL union][1] variants is restricted to a concrete [`ScalarValue`] type +/// in its implementation. To resolve such problem, a concrete [`ScalarValue`] +/// type should be specified with a `scalar` attribute's argument. /// /// ``` /// # use juniper::{DefaultScalarValue, GraphQLObject, GraphQLUnion}; /// # /// #[derive(GraphQLObject)] -/// #[graphql(Scalar = DefaultScalarValue)] +/// #[graphql(scalar = DefaultScalarValue)] /// struct Human { /// id: String, /// home_planet: String, @@ -1051,7 +1318,7 @@ pub fn graphql_interface(attr: TokenStream, body: TokenStream) -> TokenStream { /// /// // NOTICE: Removing `Scalar` argument will fail compilation. /// #[derive(GraphQLUnion)] -/// #[graphql(Scalar = DefaultScalarValue)] +/// #[graphql(scalar = DefaultScalarValue)] /// enum Character { /// Human(Human), /// Droid(Droid), @@ -1060,8 +1327,8 @@ pub fn graphql_interface(attr: TokenStream, body: TokenStream) -> TokenStream { /// /// # Ignoring enum variants /// -/// To omit exposing an enum variant in the GraphQL schema, use an `ignore`/`skip` attribute's -/// argument directly on that variant. +/// To omit exposing an enum variant in the GraphQL schema, use an `ignore` +/// attribute's argument directly on that variant. /// /// > __WARNING__: /// > It's the _library user's responsibility_ to ensure that ignored enum variant is _never_ @@ -1089,7 +1356,7 @@ pub fn graphql_interface(attr: TokenStream, body: TokenStream) -> TokenStream { /// Human(Human), /// Droid(Droid), /// #[from(ignore)] -/// #[graphql(ignore)] // or `#[graphql(skip)]`, your choice +/// #[graphql(ignore)] /// _State(PhantomData), /// } /// ``` @@ -1215,8 +1482,8 @@ pub fn graphql_interface(attr: TokenStream, body: TokenStream) -> TokenStream { /// [4]: https://doc.rust-lang.org/stable/std/primitive.unit.html #[proc_macro_error] #[proc_macro_derive(GraphQLUnion, attributes(graphql))] -pub fn derive_union(input: TokenStream) -> TokenStream { - self::graphql_union::derive::expand(input.into()) +pub fn derive_union(body: TokenStream) -> TokenStream { + self::graphql_union::derive::expand(body.into()) .unwrap_or_abort() .into() } @@ -1319,7 +1586,7 @@ pub fn derive_union(input: TokenStream) -> TokenStream { /// trait methods, and uses [unit type `()`][4] if signatures contains no [`Context`] arguments. /// /// If [`Context`] type cannot be inferred or is inferred incorrectly, then specify it explicitly -/// with `context`/`Context` attribute's argument. +/// with `context` attribute's argument. /// /// ``` /// # use std::collections::HashMap; @@ -1366,17 +1633,17 @@ pub fn derive_union(input: TokenStream) -> TokenStream { /// /// # Custom `ScalarValue` /// -/// By default, `#[graphql_union]` macro generates code, which is generic over a [`ScalarValue`] -/// type. This may introduce a problem when at least one of [GraphQL union][1] variants is -/// restricted to a concrete [`ScalarValue`] type in its implementation. To resolve such problem, a -/// concrete [`ScalarValue`] type should be specified with a `scalar`/`Scalar`/`ScalarValue` -/// attribute's argument. +/// By default, `#[graphql_union]` macro generates code, which is generic over +/// a [`ScalarValue`] type. This may introduce a problem when at least one of +/// [GraphQL union][1] variants is restricted to a concrete [`ScalarValue`] type +/// in its implementation. To resolve such problem, a concrete [`ScalarValue`] +/// type should be specified with a `scalar` attribute's argument. /// /// ``` /// # use juniper::{graphql_union, DefaultScalarValue, GraphQLObject}; /// # /// #[derive(GraphQLObject)] -/// #[graphql(Scalar = DefaultScalarValue)] +/// #[graphql(scalar = DefaultScalarValue)] /// struct Human { /// id: String, /// home_planet: String, @@ -1389,7 +1656,7 @@ pub fn derive_union(input: TokenStream) -> TokenStream { /// } /// /// // NOTICE: Removing `Scalar` argument will fail compilation. -/// #[graphql_union(Scalar = DefaultScalarValue)] +/// #[graphql_union(scalar = DefaultScalarValue)] /// trait Character { /// fn as_human(&self) -> Option<&Human> { None } /// fn as_droid(&self) -> Option<&Droid> { None } @@ -1401,8 +1668,8 @@ pub fn derive_union(input: TokenStream) -> TokenStream { /// /// # Ignoring trait methods /// -/// To omit some trait method to be assumed as a [GraphQL union][1] variant and ignore it, use an -/// `ignore`/`skip` attribute's argument directly on that method. +/// To omit some trait method to be assumed as a [GraphQL union][1] variant and +/// ignore it, use an `ignore` attribute's argument directly on that method. /// /// ``` /// # use juniper::{graphql_union, GraphQLObject}; @@ -1423,7 +1690,7 @@ pub fn derive_union(input: TokenStream) -> TokenStream { /// trait Character { /// fn as_human(&self) -> Option<&Human> { None } /// fn as_droid(&self) -> Option<&Droid> { None } -/// #[graphql(ignore)] // or `#[graphql(skip)]`, your choice +/// #[graphql(ignore)] /// fn id(&self) -> &str; /// } /// # diff --git a/juniper_codegen/src/result.rs b/juniper_codegen/src/result.rs index 68c878ffa..f15e180ef 100644 --- a/juniper_codegen/src/result.rs +++ b/juniper_codegen/src/result.rs @@ -11,22 +11,22 @@ pub const SPEC_URL: &str = "https://spec.graphql.org/June2018/"; #[allow(unused_variables)] pub enum GraphQLScope { InterfaceAttr, + ObjectAttr, + ObjectDerive, UnionAttr, UnionDerive, - DeriveObject, DeriveInputObject, DeriveEnum, DeriveScalar, ImplScalar, - ImplObject, } impl GraphQLScope { pub fn spec_section(&self) -> &str { match self { Self::InterfaceAttr => "#sec-Interfaces", + Self::ObjectAttr | Self::ObjectDerive => "#sec-Objects", Self::UnionAttr | Self::UnionDerive => "#sec-Unions", - Self::DeriveObject | Self::ImplObject => "#sec-Objects", Self::DeriveInputObject => "#sec-Input-Objects", Self::DeriveEnum => "#sec-Enums", Self::DeriveScalar | Self::ImplScalar => "#sec-Scalars", @@ -38,8 +38,8 @@ impl fmt::Display for GraphQLScope { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let name = match self { Self::InterfaceAttr => "interface", + Self::ObjectAttr | Self::ObjectDerive => "object", Self::UnionAttr | Self::UnionDerive => "union", - Self::DeriveObject | Self::ImplObject => "object", Self::DeriveInputObject => "input object", Self::DeriveEnum => "enum", Self::DeriveScalar | Self::ImplScalar => "scalar", @@ -69,6 +69,11 @@ impl GraphQLScope { .note(self.spec_link()) } + pub fn error(&self, err: syn::Error) -> Diagnostic { + Diagnostic::spanned(err.span(), Level::Error, format!("{} {}", self, err)) + .note(self.spec_link()) + } + pub fn emit_custom>(&self, span: Span, msg: S) { self.custom(span, msg).emit() } diff --git a/juniper_codegen/src/util/mod.rs b/juniper_codegen/src/util/mod.rs index 63ac9c5b2..5648b8c30 100644 --- a/juniper_codegen/src/util/mod.rs +++ b/juniper_codegen/src/util/mod.rs @@ -1,10 +1,9 @@ #![allow(clippy::single_match)] pub mod duplicate; -pub mod parse_impl; pub mod span_container; -use std::{collections::HashMap, str::FromStr}; +use std::{collections::HashMap, convert::TryFrom, str::FromStr}; use proc_macro2::{Span, TokenStream}; use proc_macro_error::abort; @@ -21,61 +20,12 @@ use syn::{ use crate::common::parse::ParseBufferExt as _; -/// Returns the name of a type. -/// If the type does not end in a simple ident, `None` is returned. -pub fn name_of_type(ty: &syn::Type) -> Option { - let path_opt = match ty { - syn::Type::Path(ref type_path) => Some(&type_path.path), - syn::Type::Reference(ref reference) => match &*reference.elem { - syn::Type::Path(ref type_path) => Some(&type_path.path), - syn::Type::TraitObject(ref trait_obj) => { - match trait_obj.bounds.iter().next().unwrap() { - syn::TypeParamBound::Trait(ref trait_bound) => Some(&trait_bound.path), - _ => None, - } - } - _ => None, - }, - _ => None, - }; - let path = path_opt?; - - path.segments - .iter() - .last() - .map(|segment| segment.ident.clone()) -} - /// Compares a path to a one-segment string value, /// return true if equal. pub fn path_eq_single(path: &syn::Path, value: &str) -> bool { path.segments.len() == 1 && path.segments[0].ident == value } -/// Check if a type is a reference to another type. -pub fn type_is_ref_of(ty: &syn::Type, target: &syn::Type) -> bool { - match ty { - syn::Type::Reference(_ref) => &*_ref.elem == target, - _ => false, - } -} - -/// Check if a Type is a simple identifier. -pub fn type_is_identifier(ty: &syn::Type, name: &str) -> bool { - match ty { - syn::Type::Path(ref type_path) => path_eq_single(&type_path.path, name), - _ => false, - } -} - -/// Check if a Type is a reference to a given identifier. -pub fn type_is_identifier_ref(ty: &syn::Type, name: &str) -> bool { - match ty { - syn::Type::Reference(_ref) => type_is_identifier(&*_ref.elem, name), - _ => false, - } -} - #[derive(Debug)] pub struct DeprecationAttr { pub reason: Option, @@ -295,7 +245,7 @@ pub fn is_valid_name(field_name: &str) -> bool { } /// The different possible ways to change case of fields in a struct, or variants in an enum. -#[derive(Copy, Clone, PartialEq, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum RenameRule { /// Don't apply a default rename rule. None, @@ -328,6 +278,20 @@ impl FromStr for RenameRule { } } +impl TryFrom for RenameRule { + type Error = syn::Error; + + fn try_from(lit: syn::LitStr) -> syn::Result { + Self::from_str(&lit.value()).map_err(|_| syn::Error::new(lit.span(), "unknown rename rule")) + } +} + +impl Parse for RenameRule { + fn parse(input: ParseStream<'_>) -> syn::Result { + Self::try_from(input.parse::()?) + } +} + #[derive(Default, Debug)] pub struct ObjectAttributes { pub name: Option>, @@ -341,7 +305,7 @@ pub struct ObjectAttributes { } impl Parse for ObjectAttributes { - fn parse(input: ParseStream) -> syn::Result { + fn parse(input: ParseStream<'_>) -> syn::Result { let mut output = Self::default(); while !input.is_empty() { @@ -400,13 +364,8 @@ impl Parse for ObjectAttributes { output.is_internal = true; } "rename" => { - input.parse::()?; - let val = input.parse::()?; - if let Ok(rename) = RenameRule::from_str(&val.value()) { - output.rename = Some(rename); - } else { - return Err(syn::Error::new(val.span(), "unknown rename rule")); - } + input.parse::()?; + output.rename = Some(input.parse::()?); } _ => { return Err(syn::Error::new(ident.span(), "unknown attribute")); @@ -449,7 +408,7 @@ pub struct FieldAttributeArgument { } impl Parse for FieldAttributeArgument { - fn parse(input: ParseStream) -> syn::Result { + fn parse(input: ParseStream<'_>) -> syn::Result { let name = input.parse::()?.unraw(); let mut arg = Self { @@ -490,7 +449,6 @@ impl Parse for FieldAttributeArgument { #[derive(PartialEq, Eq, Clone, Copy, Debug)] pub enum FieldAttributeParseMode { Object, - Impl, } enum FieldAttribute { @@ -503,7 +461,7 @@ enum FieldAttribute { } impl Parse for FieldAttribute { - fn parse(input: ParseStream) -> syn::Result { + fn parse(input: ParseStream<'_>) -> syn::Result { let ident = input.parse::()?; match ident.to_string().as_str() { @@ -593,7 +551,7 @@ pub struct FieldAttributes { } impl Parse for FieldAttributes { - fn parse(input: ParseStream) -> syn::Result { + fn parse(input: ParseStream<'_>) -> syn::Result { let items = Punctuated::::parse_terminated(&input)?; let mut output = Self::default(); @@ -654,10 +612,6 @@ impl FieldAttributes { Ok(output) } - - pub fn argument(&self, name: &str) -> Option<&FieldAttributeArgument> { - self.arguments.get(name) - } } #[derive(Debug)] @@ -729,710 +683,6 @@ impl GraphQLTypeDefiniton { self.fields.iter().any(|field| field.is_async) } - pub fn into_tokens(self) -> TokenStream { - let name = &self.name; - let ty = &self._type; - let context = self - .context - .as_ref() - .map(|ctx| quote!( #ctx )) - .unwrap_or_else(|| quote!(())); - - let field_definitions = self.fields.iter().map(|field| { - let args = field.args.iter().map(|arg| { - let arg_type = &arg._type; - let arg_name = &arg.name; - - let description = match arg.description.as_ref() { - Some(value) => quote!( .description( #value ) ), - None => quote!(), - }; - - // Code. - match arg.default.as_ref() { - Some(value) => quote!( - .argument( - registry.arg_with_default::<#arg_type>(#arg_name, &#value, info) - #description - ) - ), - None => quote!( - .argument( - registry.arg::<#arg_type>(#arg_name, info) - #description - ) - ), - } - }); - - let description = match field.description.as_ref() { - Some(description) => quote!( .description(#description) ), - None => quote!(), - }; - - let deprecation = match field.deprecation.as_ref() { - Some(deprecation) => { - if let Some(reason) = deprecation.reason.as_ref() { - quote!( .deprecated(Some(#reason)) ) - } else { - quote!( .deprecated(None) ) - } - } - None => quote!(), - }; - - let field_name = &field.name; - - let _type = &field._type; - quote! { - registry - .field_convert::<#_type, _, Self::Context>(#field_name, info) - #(#args)* - #description - #deprecation - } - }); - - let scalar = self - .scalar - .as_ref() - .map(|s| quote!( #s )) - .unwrap_or_else(|| { - if self.generic_scalar { - // If generic_scalar is true, we always insert a generic scalar. - // See more comments below. - quote!(__S) - } else { - quote!(::juniper::DefaultScalarValue) - } - }); - - let resolve_matches = self.fields.iter().map(|field| { - let name = &field.name; - let code = &field.resolver_code; - - if field.is_async { - quote!( - #name => { - panic!("Tried to resolve async field {} on type {:?} with a sync resolver", - #name, - >::name(_info) - ); - }, - ) - } else { - let _type = if field.is_type_inferred { - quote!() - } else { - let _type = &field._type; - quote!(: #_type) - }; - quote!( - #name => { - let res #_type = (|| { #code })(); - ::juniper::IntoResolvable::into( - res, - executor.context() - ) - .and_then(|res| { - match res { - Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(&(), &r), - None => Ok(::juniper::Value::null()), - } - }) - }, - ) - } - }); - - let description = self - .description - .as_ref() - .map(|description| quote!( .description(#description) )); - - let interfaces = if !self.interfaces.is_empty() { - let interfaces_ty = &self.interfaces; - - Some(quote!( - .interfaces(&[ - #( registry.get_type::<#interfaces_ty>(&()) ,)* - ]) - )) - } else { - None - }; - - // Preserve the original type_generics before modification, - // since alteration makes them invalid if self.generic_scalar - // is specified. - let (_, type_generics, _) = self.generics.split_for_impl(); - - let mut generics = self.generics.clone(); - - if self.scalar.is_none() && self.generic_scalar { - // No custom scalar specified, but always generic specified. - // Therefore we inject the generic scalar. - generics.params.push(parse_quote!(__S)); - generics - .make_where_clause() - .predicates - .push(parse_quote!(__S: ::juniper::ScalarValue)); - } - - let type_generics_tokens = if self.include_type_generics { - Some(type_generics) - } else { - None - }; - let (impl_generics, _, where_clause) = generics.split_for_impl(); - - let resolve_field_async = { - let resolve_matches_async = self.fields.iter().map(|field| { - let name = &field.name; - let code = &field.resolver_code; - let _type = if field.is_type_inferred { - quote!() - } else { - let _type = &field._type; - quote!(: #_type) - }; - - if field.is_async { - quote!( - #name => { - let f = async move { - let res #_type = async move { #code }.await; - - let inner_res = ::juniper::IntoResolvable::into( - res, - executor.context() - ); - match inner_res { - Ok(Some((ctx, r))) => { - let subexec = executor - .replaced_context(ctx); - subexec.resolve_with_ctx_async(&(), &r) - .await - }, - Ok(None) => Ok(::juniper::Value::null()), - Err(e) => Err(e), - } - }; - Box::pin(f) - }, - ) - } else { - let inner = if !self.no_async { - quote!( - let f = async move { - match res2 { - Ok(Some((ctx, r))) => { - let sub = executor.replaced_context(ctx); - sub.resolve_with_ctx_async(&(), &r).await - }, - Ok(None) => Ok(::juniper::Value::null()), - Err(e) => Err(e), - } - }; - use ::juniper::futures::future; - future::FutureExt::boxed(f) - ) - } else { - quote!( - let v = match res2 { - Ok(Some((ctx, r))) => executor.replaced_context(ctx).resolve_with_ctx(&(), &r), - Ok(None) => Ok(::juniper::Value::null()), - Err(e) => Err(e), - }; - use ::juniper::futures::future; - Box::pin(future::ready(v)) - ) - }; - - quote!( - #name => { - let res #_type = (||{ #code })(); - let res2 = ::juniper::IntoResolvable::into( - res, - executor.context() - ); - #inner - }, - ) - } - }); - - let mut where_async = where_clause.cloned().unwrap_or_else(|| parse_quote!(where)); - - where_async - .predicates - .push(parse_quote!( #scalar: Send + Sync )); - where_async.predicates.push(parse_quote!(Self: Sync)); - - let as_dyn_value = if !self.interfaces.is_empty() { - Some(quote! { - #[automatically_derived] - impl#impl_generics ::juniper::AsDynGraphQLValue<#scalar> for #ty #type_generics_tokens - #where_async - { - type Context = >::Context; - type TypeInfo = >::TypeInfo; - - #[inline] - fn as_dyn_graphql_value(&self) -> &::juniper::DynGraphQLValue<#scalar, Self::Context, Self::TypeInfo> { - self - } - - #[inline] - fn as_dyn_graphql_value_async(&self) -> &::juniper::DynGraphQLValueAsync<#scalar, Self::Context, Self::TypeInfo> { - self - } - } - }) - } else { - None - }; - - quote!( - impl#impl_generics ::juniper::GraphQLValueAsync<#scalar> for #ty #type_generics_tokens - #where_async - { - fn resolve_field_async<'b>( - &'b self, - info: &'b Self::TypeInfo, - field: &'b str, - args: &'b ::juniper::Arguments<#scalar>, - executor: &'b ::juniper::Executor, - ) -> ::juniper::BoxFuture<'b, ::juniper::ExecutionResult<#scalar>> - where #scalar: Send + Sync, - { - use ::juniper::futures::future; - use ::juniper::GraphQLType; - match field { - #( #resolve_matches_async )* - _ => { - panic!("Field {} not found on type {:?}", - field, - >::name(info) - ); - } - } - } - } - - #as_dyn_value - ) - }; - - let marks = self.fields.iter().map(|field| { - let field_marks = field.args.iter().map(|arg| { - let arg_ty = &arg._type; - quote! { <#arg_ty as ::juniper::marker::IsInputType<#scalar>>::mark(); } - }); - - let field_ty = &field._type; - let resolved_ty = quote! { - <#field_ty as ::juniper::IntoResolvable< - '_, #scalar, _, >::Context, - >>::Type - }; - - quote! { - #( #field_marks )* - <#resolved_ty as ::juniper::marker::IsOutputType<#scalar>>::mark(); - } - }); - - let output = quote!( - impl#impl_generics ::juniper::marker::IsOutputType<#scalar> for #ty #type_generics_tokens #where_clause { - fn mark() { - #( #marks )* - } - } - - impl#impl_generics ::juniper::marker::GraphQLObjectType<#scalar> for #ty #type_generics_tokens #where_clause - { } - - impl#impl_generics ::juniper::GraphQLType<#scalar> for #ty #type_generics_tokens - #where_clause - { - fn name(_: &Self::TypeInfo) -> Option<&'static str> { - Some(#name) - } - - fn meta<'r>( - info: &Self::TypeInfo, - registry: &mut ::juniper::Registry<'r, #scalar> - ) -> ::juniper::meta::MetaType<'r, #scalar> - where #scalar : 'r, - { - let fields = [ - #( #field_definitions ),* - ]; - let meta = registry.build_object_type::<#ty>(info, &fields) - #description - #interfaces; - meta.into_meta() - } - } - - impl#impl_generics ::juniper::GraphQLValue<#scalar> for #ty #type_generics_tokens - #where_clause - { - type Context = #context; - type TypeInfo = (); - - fn type_name<'__i>(&self, info: &'__i Self::TypeInfo) -> Option<&'__i str> { - >::name(info) - } - - #[allow(unused_variables)] - #[allow(unused_mut)] - fn resolve_field( - &self, - _info: &(), - field: &str, - args: &::juniper::Arguments<#scalar>, - executor: &::juniper::Executor, - ) -> ::juniper::ExecutionResult<#scalar> { - match field { - #( #resolve_matches )* - _ => { - panic!("Field {} not found on type {:?}", - field, - >::name(_info) - ); - } - } - } - - - fn concrete_type_name(&self, _: &Self::Context, _: &Self::TypeInfo) -> String { - #name.to_string() - } - - } - - #resolve_field_async - ); - output - } - - pub fn into_subscription_tokens(self) -> TokenStream { - let name = &self.name; - let ty = &self._type; - let context = self - .context - .as_ref() - .map(|ctx| quote!( #ctx )) - .unwrap_or_else(|| quote!(())); - - let scalar = self - .scalar - .as_ref() - .map(|s| quote!( #s )) - .unwrap_or_else(|| { - if self.generic_scalar { - // If generic_scalar is true, we always insert a generic scalar. - // See more comments below. - quote!(__S) - } else { - quote!(::juniper::DefaultScalarValue) - } - }); - - let field_definitions = self.fields.iter().map(|field| { - let args = field.args.iter().map(|arg| { - let arg_type = &arg._type; - let arg_name = &arg.name; - - let description = match arg.description.as_ref() { - Some(value) => quote!( .description( #value ) ), - None => quote!(), - }; - - match arg.default.as_ref() { - Some(value) => quote!( - .argument( - registry.arg_with_default::<#arg_type>(#arg_name, &#value, info) - #description - ) - ), - None => quote!( - .argument( - registry.arg::<#arg_type>(#arg_name, info) - #description - ) - ), - } - }); - - let description = match field.description.as_ref() { - Some(description) => quote!( .description(#description) ), - None => quote!(), - }; - - let deprecation = match field.deprecation.as_ref() { - Some(deprecation) => { - if let Some(reason) = deprecation.reason.as_ref() { - quote!( .deprecated(Some(#reason)) ) - } else { - quote!( .deprecated(None) ) - } - } - None => quote!(), - }; - - let field_name = &field.name; - - let type_name = &field._type; - - let _type; - - if field.is_async { - _type = quote!(<#type_name as ::juniper::ExtractTypeFromStream<_, #scalar>>::Item); - } else { - panic!("Synchronous resolvers are not supported. Specify that this function is async: 'async fn foo()'") - } - - quote! { - registry - .field_convert::<#_type, _, Self::Context>(#field_name, info) - #(#args)* - #description - #deprecation - } - }); - - let description = self - .description - .as_ref() - .map(|description| quote!( .description(#description) )); - - let interfaces = if !self.interfaces.is_empty() { - let interfaces_ty = &self.interfaces; - - Some(quote!( - .interfaces(&[ - #( registry.get_type::<#interfaces_ty>(&()) ,)* - ]) - )) - } else { - None - }; - - // Preserve the original type_generics before modification, - // since alteration makes them invalid if self.generic_scalar - // is specified. - let (_, type_generics, _) = self.generics.split_for_impl(); - - let mut generics = self.generics.clone(); - - if self.scalar.is_none() && self.generic_scalar { - // No custom scalar specified, but always generic specified. - // Therefore we inject the generic scalar. - - // Insert ScalarValue constraint. - generics.params.push(parse_quote!(__S)); - generics - .make_where_clause() - .predicates - .push(parse_quote!(__S: ::juniper::ScalarValue)); - } - - let type_generics_tokens = if self.include_type_generics { - Some(type_generics) - } else { - None - }; - let (impl_generics, _, where_clause) = generics.split_for_impl(); - - let mut generics_with_send_sync = generics.clone(); - if self.scalar.is_none() && self.generic_scalar { - generics_with_send_sync - .make_where_clause() - .predicates - .push(parse_quote!(__S: Send + Sync)); - } - let (_, _, where_clause_with_send_sync) = generics_with_send_sync.split_for_impl(); - - let resolve_matches_async = self.fields.iter().filter(|field| field.is_async).map( - |field| { - let name = &field.name; - let code = &field.resolver_code; - - let _type; - if field.is_type_inferred { - _type = quote!(); - } else { - let _type_name = &field._type; - _type = quote!(: #_type_name); - }; - quote!( - #name => { - ::juniper::futures::FutureExt::boxed(async move { - let res #_type = async { #code }.await; - let res = ::juniper::IntoFieldResult::<_, #scalar>::into_result(res)?; - let executor= executor.as_owned_executor(); - let f = res.then(move |res| { - let executor = executor.clone(); - let res2: ::juniper::FieldResult<_, #scalar> = - ::juniper::IntoResolvable::into(res, executor.context()); - async move { - let ex = executor.as_executor(); - match res2 { - Ok(Some((ctx, r))) => { - let sub = ex.replaced_context(ctx); - sub.resolve_with_ctx_async(&(), &r) - .await - .map_err(|e| ex.new_error(e)) - } - Ok(None) => Ok(Value::null()), - Err(e) => Err(ex.new_error(e)), - } - } - }); - Ok( - ::juniper::Value::Scalar::< - ::juniper::ValuesStream::<#scalar> - >(Box::pin(f)) - ) - }) - } - ) - }, - ); - - let marks = self.fields.iter().map(|field| { - let field_marks = field.args.iter().map(|arg| { - let arg_ty = &arg._type; - quote! { <#arg_ty as ::juniper::marker::IsInputType<#scalar>>::mark(); } - }); - - let field_ty = &field._type; - let stream_item_ty = quote! { - <#field_ty as ::juniper::IntoFieldResult::<_, #scalar>>::Item - }; - let resolved_ty = quote! { - <#stream_item_ty as ::juniper::IntoResolvable< - '_, #scalar, _, >::Context, - >>::Type - }; - - quote! { - #( #field_marks )* - <#resolved_ty as ::juniper::marker::IsOutputType<#scalar>>::mark(); - } - }); - - let graphql_implementation = quote!( - impl#impl_generics ::juniper::marker::IsOutputType<#scalar> for #ty #type_generics_tokens - #where_clause - { - fn mark() { - #( #marks )* - } - } - - impl#impl_generics ::juniper::GraphQLType<#scalar> for #ty #type_generics_tokens - #where_clause - { - fn name(_: &Self::TypeInfo) -> Option<&'static str> { - Some(#name) - } - - fn meta<'r>( - info: &Self::TypeInfo, - registry: &mut ::juniper::Registry<'r, #scalar> - ) -> ::juniper::meta::MetaType<'r, #scalar> - where #scalar : 'r, - { - let fields = [ - #( #field_definitions ),* - ]; - let meta = registry.build_object_type::<#ty>(info, &fields) - #description - #interfaces; - meta.into_meta() - } - } - - impl#impl_generics ::juniper::GraphQLValue<#scalar> for #ty #type_generics_tokens - #where_clause - { - type Context = #context; - type TypeInfo = (); - - fn type_name<'__i>(&self, info: &'__i Self::TypeInfo) -> Option<&'__i str> { - >::name(info) - } - - fn resolve_field( - &self, - _: &(), - _: &str, - _: &::juniper::Arguments<#scalar>, - _: &::juniper::Executor, - ) -> ::juniper::ExecutionResult<#scalar> { - panic!("Called `resolve_field` on subscription object"); - } - - - fn concrete_type_name(&self, _: &Self::Context, _: &Self::TypeInfo) -> String { - #name.to_string() - } - } - ); - - let subscription_implementation = quote!( - impl#impl_generics ::juniper::GraphQLSubscriptionValue<#scalar> for #ty #type_generics_tokens - #where_clause_with_send_sync - { - #[allow(unused_variables)] - fn resolve_field_into_stream< - 's, 'i, 'fi, 'args, 'e, 'ref_e, 'res, 'f, - >( - &'s self, - info: &'i Self::TypeInfo, - field_name: &'fi str, - args: ::juniper::Arguments<'args, #scalar>, - executor: &'ref_e ::juniper::Executor<'ref_e, 'e, Self::Context, #scalar>, - ) -> std::pin::Pin>, - ::juniper::FieldError<#scalar> - > - > + Send + 'f - >> - where - 's: 'f, - 'i: 'res, - 'fi: 'f, - 'e: 'res, - 'args: 'f, - 'ref_e: 'f, - 'res: 'f, - { - use ::juniper::Value; - use ::juniper::futures::stream::StreamExt as _; - - match field_name { - #( #resolve_matches_async )* - _ => { - panic!("Field {} not found on type {}", field_name, "GraphQLSubscriptionValue"); - } - } - } - } - ); - - quote!( - #graphql_implementation - #subscription_implementation - ) - } - pub fn into_enum_tokens(self) -> TokenStream { let name = &self.name; let ty = &self._type; @@ -1728,11 +978,10 @@ impl GraphQLTypeDefiniton { quote!( #field_ident: { - // TODO: investigate the unwraps here, they seem dangerous! match obj.get(#field_name) { #from_input_default Some(ref v) => ::juniper::FromInputValue::from_input_value(v)?, - None => ::juniper::FromInputValue::<#scalar>::from_implicit_null(), + None => ::juniper::FromInputValue::<#scalar>::from_implicit_null()?, } }, ) diff --git a/juniper_codegen/src/util/parse_impl.rs b/juniper_codegen/src/util/parse_impl.rs deleted file mode 100644 index db29d9e16..000000000 --- a/juniper_codegen/src/util/parse_impl.rs +++ /dev/null @@ -1,186 +0,0 @@ -//! Parse impl blocks. -#![allow(clippy::or_fun_call)] - -use crate::util::{self, span_container::SpanContainer}; -use proc_macro2::{Ident, TokenStream}; -use quote::quote; -use syn::{spanned::Spanned, PatType}; - -pub struct ImplBlock { - pub attrs: util::ObjectAttributes, - pub target_trait: Option<(String, syn::Path)>, - pub target_type: Box, - pub type_ident: syn::Ident, - pub generics: syn::Generics, - // _impl: syn::ItemImpl, - pub methods: Vec, - pub description: Option, -} - -impl ImplBlock { - /// Parse a `fn () -> ` method declaration found in - /// objects. - pub fn parse_method< - F: Fn( - &PatType, - &Ident, - bool, - ) -> syn::Result<(TokenStream, util::GraphQLTypeDefinitionFieldArg)>, - >( - &self, - method: &syn::ImplItemMethod, - is_self_optional: bool, - f: F, - ) -> syn::Result<(Vec, Vec)> { - let mut arguments = method.sig.inputs.iter().peekable(); - - // Verify `&self` argument. - match arguments.peek() { - Some(syn::FnArg::Receiver(rec)) => { - let _consume = arguments.next(); - if rec.reference.is_none() || rec.mutability.is_some() { - return Err(syn::Error::new( - rec.span(), - "invalid argument: did you mean `&self`?", - )); - } - } - _ => { - if !is_self_optional { - return Err(syn::Error::new( - method.sig.span(), - "expected a `&self` argument", - )); - } - } - } - - let mut resolve_parts = Vec::new(); - let mut additional_arguments = Vec::new(); - - for arg in arguments { - match arg { - syn::FnArg::Receiver(_) => { - if !is_self_optional { - return Err(syn::Error::new( - method.sig.ident.span(), - "self receiver must be the first argument", - )); - } - } - syn::FnArg::Typed(captured) => { - let (arg_ident, is_mut) = match &*captured.pat { - syn::Pat::Ident(ref pat_ident) => { - (&pat_ident.ident, pat_ident.mutability.is_some()) - } - _ => { - return Err(syn::Error::new( - captured.pat.span(), - "expected identifier for function argument", - )); - } - }; - let context_type = self.attrs.context.as_ref(); - - // Check for executor arguments. - if util::type_is_identifier_ref(&captured.ty, "Executor") { - resolve_parts.push(quote!(let #arg_ident = executor;)); - } - // Make sure executor is specified as a reference. - else if util::type_is_identifier(&captured.ty, "Executor") { - return Err(syn::Error::new( - captured.ty.span(), - "to access the Executor, you need to specify the type as a reference.\nDid you mean &Executor?" - )); - } - // Check for context arg. - else if context_type - .clone() - .map(|ctx| util::type_is_ref_of(&captured.ty, ctx)) - .unwrap_or(false) - { - resolve_parts.push(quote!( let #arg_ident = executor.context(); )); - } - // Make sure the user does not specify the Context - // without a reference. (&Context) - else if context_type - .clone() - .map(|ctx| ctx.inner() == &*captured.ty) - .unwrap_or(false) - { - return Err(syn::Error::new( - captured.ty.span(), - format!("to access the context, you need to specify the type as a reference.\nDid you mean &{}?", quote!(captured.ty)), - )); - } else { - let (tokens, ty) = f(captured, arg_ident, is_mut)?; - resolve_parts.push(tokens); - additional_arguments.push(ty); - } - } - } - } - - Ok((resolve_parts, additional_arguments)) - } - - pub fn parse(attr_tokens: TokenStream, body: TokenStream) -> syn::Result { - let attrs = syn::parse2::(attr_tokens)?; - let mut _impl = syn::parse2::(body)?; - - let target_trait = match _impl.clone().trait_ { - Some((_, path, _)) => { - let name = path - .segments - .iter() - .map(|segment| segment.ident.to_string()) - .collect::>() - .join("."); - Some((name, path)) - } - None => None, - }; - - let type_ident = if let Some(ident) = util::name_of_type(&*_impl.self_ty) { - ident - } else { - return Err(syn::Error::new( - _impl.self_ty.span(), - "could not determine a name for the impl type", - )); - }; - - let target_type = _impl.self_ty.clone(); - - let description = attrs - .description - .clone() - .or_else(|| util::get_doc_comment(&_impl.attrs.clone())); - - let mut methods = Vec::new(); - - for item in _impl.items { - match item { - syn::ImplItem::Method(method) => { - methods.push(method); - } - _ => { - return Err(syn::Error::new( - item.span(), - "only type declarations and methods are allowed", - )); - } - } - } - - Ok(Self { - attrs, - type_ident, - target_trait, - target_type, - generics: _impl.generics, - description: description.map(SpanContainer::into_inner), - methods, - }) - } -} diff --git a/juniper_graphql_ws/src/lib.rs b/juniper_graphql_ws/src/lib.rs index 6ad25029a..f5627596e 100644 --- a/juniper_graphql_ws/src/lib.rs +++ b/juniper_graphql_ws/src/lib.rs @@ -642,6 +642,8 @@ mod test { struct Context(i32); + impl juniper::Context for Context {} + struct Query; #[graphql_object(context = Context)] @@ -657,7 +659,7 @@ mod test { #[graphql_subscription(context = Context)] impl Subscription { /// never never emits anything. - async fn never(context: &Context) -> BoxStream<'static, FieldResult> { + async fn never(_context: &Context) -> BoxStream<'static, FieldResult> { tokio::time::sleep(Duration::from_secs(10000)) .map(|_| unreachable!()) .into_stream() @@ -676,7 +678,7 @@ mod test { } /// error emits an error once, then never emits anything else. - async fn error(context: &Context) -> BoxStream<'static, FieldResult> { + async fn error(_context: &Context) -> BoxStream<'static, FieldResult> { stream::once(future::ready(Err(FieldError::new( "field error", Value::null(), diff --git a/juniper_hyper/Cargo.toml b/juniper_hyper/Cargo.toml index 11f034431..53aa080e6 100644 --- a/juniper_hyper/Cargo.toml +++ b/juniper_hyper/Cargo.toml @@ -11,13 +11,13 @@ repository = "https://github.com/graphql-rust/juniper" [dependencies] futures = "0.3.1" juniper = { version = "0.15.7", path = "../juniper", default-features = false } -hyper = {version = "0.14", features = ["server", "runtime"]} +hyper = { version = "0.14", features = ["server", "runtime"] } serde_json = "1.0" -tokio = "1" -url = "2" +tokio = "1.0" +url = "2.0" [dev-dependencies] juniper = { version = "0.15.7", path = "../juniper", features = ["expose-test-schema"] } pretty_env_logger = "0.4" reqwest = { version = "0.11", features = ["blocking", "rustls-tls"] } -tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } diff --git a/juniper_iron/src/lib.rs b/juniper_iron/src/lib.rs index b58bcd7aa..118c64862 100644 --- a/juniper_iron/src/lib.rs +++ b/juniper_iron/src/lib.rs @@ -44,9 +44,9 @@ use juniper::{Context, EmptyMutation, EmptySubscription}; # Ok(&self.name) # } # -# fn friends(&self, context: &Database) -> FieldResult> { +# fn friends<'c>(&self, context: &'c Database) -> FieldResult> { # Ok(self.friend_ids.iter() -# .filter_map(|id| executor.context().users.get(id)) +# .filter_map(|id| context.users.get(id)) # .collect()) # } # } @@ -54,7 +54,7 @@ use juniper::{Context, EmptyMutation, EmptySubscription}; # #[juniper::graphql_object(context = Database, scalar = juniper::DefaultScalarValue)] # impl QueryRoot { # fn user(context: &Database, id: String) -> FieldResult> { -# Ok(executor.context().users.get(&id)) +# Ok(context.users.get(&id)) # } # } diff --git a/juniper_warp/src/lib.rs b/juniper_warp/src/lib.rs index 64180d700..4c683e92f 100644 --- a/juniper_warp/src/lib.rs +++ b/juniper_warp/src/lib.rs @@ -70,6 +70,7 @@ use warp::{body, filters::BoxedFilter, http, hyper::body::Bytes, query, Filter}; /// # #[derive(Debug)] /// struct AppState(Vec); /// struct ExampleContext(Arc, UserId); +/// # impl juniper::Context for ExampleContext {} /// /// struct QueryRoot; ///