From cbf16c5a335cff2c5d6c062026c0880692743132 Mon Sep 17 00:00:00 2001 From: Kai Ren Date: Tue, 6 Oct 2020 10:21:01 +0300 Subject: [PATCH] Make interfaces great again! (#682) * Bootstrap * Upd * Bootstrap macro * Revert stuff * Correct PoC to compile * Bootstrap #[graphql_interface] expansion * Bootstrap #[graphql_interface] meta parsing * Bootstrap #[graphql_interface] very basic code generation [skip ci] * Upd trait code generation and fix keywords usage [skip ci] * Expand trait impls [skip ci] * Tune up objects [skip ci] * Finally! Complies at least... [skip ci] * Parse meta for fields and its arguments [skip ci] - also, refactor and bikeshed new macros code * Impl filling fields meta and bootstrap field resolution [skip ci] * Poking with fields resolution [skip ci] * Solve Rust's teen async HRTB problems [skip ci] * Start parsing trait methods [skip ci] * Finish parsing fields from trait methods [skip ci] * Autodetect trait asyncness and allow to specify it [skip ci] * Allow to autogenerate trait object alias via attribute * Support generics in trait definition and asyncify them correctly * Temporary disable explicit async * Cover arguments and custom names/descriptions in tests * Re-enable tests with explicit async and fix the codegen to satisfy it * Check implementers are registered in schema and vice versa * Check argument camelCases * Test argument defaults, and allow Into coercions for them * Re-enable markers * Re-enable markers and relax Sized requirement on IsInputType/IsOutputType marker traits * Revert 'juniper_actix' fmt * Fix missing marks for object * Fix subscriptions marks * Deduce result type correctly via traits * Final fixes * Fmt * Restore marks checking * Support custom ScalarValue * Cover deprecations with tests * Impl dowcasting via methods * Impl dowcasting via external functions * Support custom context, vol. 1 * Support custom context, vol. 2 * Cover fallible field with test * Impl explicit generic ScalarValue, vol.1 * Impl explicit generic ScalarValue, vol.2 * Allow passing executor into methods * Generating enum, vol.1 * Generating enum, vol.2 * Generating enum, vol.3 * Generating enum, vol.3 * Generating enum, vol.4 * Generating enum, vol.5 * Generating enum, vol.6 * Generating enum, vol.7 * Generating enum, vol.8 * Refactor juniper stuff * Fix juniper tests, vol.1 * Fix juniper tests, vol.2 * Polish 'juniper' crate changes, vol.1 * Polish 'juniper' crate changes, vol.2 * Remove redundant stuf * Polishing 'juniper_codegen', vol.1 * Polishing 'juniper_codegen', vol.2 * Polishing 'juniper_codegen', vol.3 * Polishing 'juniper_codegen', vol.4 * Polishing 'juniper_codegen', vol.5 * Polishing 'juniper_codegen', vol.6 * Polishing 'juniper_codegen', vol.7 * Polishing 'juniper_codegen', vol.8 * Polishing 'juniper_codegen', vol.9 * Fix other crates tests and make Clippy happier * Fix examples * Add codegen failure tests, vol. 1 * Add codegen failure tests, vol. 2 * Add codegen failure tests, vol.3 * Fix codegen failure tests accordingly to latest nightly Rust * Fix codegen when interface has no implementers * Fix warnings in book tests * Describing new interfaces in Book, vol.1 Co-authored-by: Christian Legnitto --- docs/book/content/advanced/introspection.md | 1 + docs/book/content/advanced/subscriptions.md | 3 +- docs/book/content/quickstart.md | 1 + .../content/schema/schemas_and_mutations.md | 2 + docs/book/content/types/input_objects.md | 2 + docs/book/content/types/interfaces.md | 291 +- .../content/types/objects/complex_fields.md | 3 +- docs/book/content/types/unions.md | 8 +- docs/book/tests/Cargo.toml | 2 +- examples/actix_subscriptions/src/main.rs | 6 +- .../interface/argument_double_underscored.rs | 19 + .../argument_double_underscored.stderr | 19 + .../fail/interface/argument_non_input_type.rs | 21 + .../interface/argument_non_input_type.stderr | 16 + ...hod_conflicts_with_external_downcast_fn.rs | 29 + ...conflicts_with_external_downcast_fn.stderr | 20 + .../downcast_method_wrong_input_args.rs | 24 + .../downcast_method_wrong_input_args.stderr | 19 + .../downcast_method_wrong_return_type.rs | 24 + .../downcast_method_wrong_return_type.stderr | 19 + .../interface/field_double_underscored.rs | 19 + .../interface/field_double_underscored.stderr | 19 + .../interface/field_non_output_return_type.rs | 24 + .../field_non_output_return_type.stderr | 8 + .../fail/interface/fields_duplicate.rs | 24 + .../fail/interface/fields_duplicate.stderr | 25 + .../fail/interface/impl_argument_no_object.rs | 23 - .../interface/impl_argument_no_object.stderr | 27 - .../impl_argument_no_underscore.rs.disabled | 23 - .../fail/interface/impl_no_fields.rs.disabled | 12 - .../impl_no_input_object.rs.disabled | 23 - .../interface/impl_no_underscore.rs.disabled | 23 - .../interface/impl_unqiue_name.rs.disabled | 29 - .../interface/implementer_non_object_type.rs | 20 + .../implementer_non_object_type.stderr | 17 + .../implementers_duplicate_pretty.rs | 21 + .../implementers_duplicate_pretty.stderr | 17 + .../interface/implementers_duplicate_ugly.rs | 23 + .../implementers_duplicate_ugly.stderr | 21 + .../fail/interface/name_double_underscored.rs | 10 + .../interface/name_double_underscored.stderr | 7 + .../codegen_fail/fail/interface/no_fields.rs | 15 + .../fail/interface/no_fields.stderr | 19 + .../codegen_fail/fail/interface/wrong_item.rs | 11 + .../fail/interface/wrong_item.stderr | 7 + .../fail/union/enum_same_type_ugly.stderr | 2 +- .../fail/union/struct_same_type_pretty.stderr | 2 +- .../fail/union/struct_same_type_ugly.stderr | 2 +- .../fail/union/trait_same_type_ugly.stderr | 2 +- integration_tests/juniper_tests/Cargo.toml | 1 + .../src/codegen/interface_attr.rs | 5259 +++++++++++++++++ .../juniper_tests/src/codegen/mod.rs | 1 + .../juniper_tests/src/codegen/union_attr.rs | 178 +- juniper/Cargo.toml | 29 +- juniper/src/executor/mod.rs | 7 +- .../src/executor_tests/introspection/mod.rs | 30 +- juniper/src/lib.rs | 17 +- juniper/src/macros/common.rs | 741 --- juniper/src/macros/helper/mod.rs | 34 + .../subscription.rs} | 0 juniper/src/macros/interface.rs | 302 - juniper/src/macros/mod.rs | 10 +- juniper/src/macros/tests/field.rs | 107 +- juniper/src/macros/tests/impl_subscription.rs | 4 +- juniper/src/macros/tests/interface.rs | 291 +- juniper/src/schema/meta.rs | 78 +- juniper/src/tests/fixtures/starwars/mod.rs | 1 - juniper/src/tests/fixtures/starwars/model.rs | 319 - juniper/src/tests/fixtures/starwars/schema.rs | 489 +- .../fixtures/starwars/schema_language.rs | 3 +- juniper/src/tests/introspection_tests.rs | 5 +- juniper/src/tests/query_tests.rs | 8 +- juniper/src/types/async_await.rs | 8 +- juniper/src/types/base.rs | 6 + juniper/src/types/marker.rs | 46 +- juniper/src/types/subscriptions.rs | 2 +- juniper_actix/examples/actix_server.rs | 2 +- juniper_actix/src/lib.rs | 4 +- juniper_codegen/src/common/gen.rs | 47 + juniper_codegen/src/common/mod.rs | 89 + juniper_codegen/src/common/parse/attr.rs | 101 + .../src/common/parse/downcaster.rs | 94 + juniper_codegen/src/common/parse/mod.rs | 261 + juniper_codegen/src/derive_enum.rs | 2 +- juniper_codegen/src/derive_input_object.rs | 2 +- juniper_codegen/src/derive_object.rs | 12 +- juniper_codegen/src/derive_scalar_value.rs | 11 +- juniper_codegen/src/graphql_interface/attr.rs | 679 +++ juniper_codegen/src/graphql_interface/mod.rs | 2316 ++++++++ juniper_codegen/src/graphql_union/attr.rs | 107 +- juniper_codegen/src/graphql_union/derive.rs | 7 +- juniper_codegen/src/graphql_union/mod.rs | 155 +- juniper_codegen/src/impl_object.rs | 18 +- juniper_codegen/src/impl_scalar.rs | 37 +- juniper_codegen/src/lib.rs | 106 + juniper_codegen/src/result.rs | 9 +- juniper_codegen/src/util/mod.rs | 164 +- juniper_codegen/src/util/option_ext.rs | 24 - juniper_hyper/examples/hyper_server.rs | 2 +- juniper_hyper/src/lib.rs | 2 +- juniper_iron/examples/iron_server.rs | 2 +- juniper_iron/src/lib.rs | 2 +- juniper_rocket/examples/rocket_server.rs | 2 +- juniper_rocket/src/lib.rs | 5 +- .../examples/rocket_server.rs | 2 +- juniper_rocket_async/src/lib.rs | 5 +- juniper_warp/examples/warp_server.rs | 2 +- juniper_warp/src/lib.rs | 6 +- 108 files changed, 10707 insertions(+), 2550 deletions(-) create mode 100644 integration_tests/codegen_fail/fail/interface/argument_double_underscored.rs create mode 100644 integration_tests/codegen_fail/fail/interface/argument_double_underscored.stderr create mode 100644 integration_tests/codegen_fail/fail/interface/argument_non_input_type.rs create mode 100644 integration_tests/codegen_fail/fail/interface/argument_non_input_type.stderr create mode 100644 integration_tests/codegen_fail/fail/interface/downcast_method_conflicts_with_external_downcast_fn.rs create mode 100644 integration_tests/codegen_fail/fail/interface/downcast_method_conflicts_with_external_downcast_fn.stderr create mode 100644 integration_tests/codegen_fail/fail/interface/downcast_method_wrong_input_args.rs create mode 100644 integration_tests/codegen_fail/fail/interface/downcast_method_wrong_input_args.stderr create mode 100644 integration_tests/codegen_fail/fail/interface/downcast_method_wrong_return_type.rs create mode 100644 integration_tests/codegen_fail/fail/interface/downcast_method_wrong_return_type.stderr create mode 100644 integration_tests/codegen_fail/fail/interface/field_double_underscored.rs create mode 100644 integration_tests/codegen_fail/fail/interface/field_double_underscored.stderr create mode 100644 integration_tests/codegen_fail/fail/interface/field_non_output_return_type.rs create mode 100644 integration_tests/codegen_fail/fail/interface/field_non_output_return_type.stderr create mode 100644 integration_tests/codegen_fail/fail/interface/fields_duplicate.rs create mode 100644 integration_tests/codegen_fail/fail/interface/fields_duplicate.stderr delete mode 100644 integration_tests/codegen_fail/fail/interface/impl_argument_no_object.rs delete mode 100644 integration_tests/codegen_fail/fail/interface/impl_argument_no_object.stderr delete mode 100644 integration_tests/codegen_fail/fail/interface/impl_argument_no_underscore.rs.disabled delete mode 100644 integration_tests/codegen_fail/fail/interface/impl_no_fields.rs.disabled delete mode 100644 integration_tests/codegen_fail/fail/interface/impl_no_input_object.rs.disabled delete mode 100644 integration_tests/codegen_fail/fail/interface/impl_no_underscore.rs.disabled delete mode 100644 integration_tests/codegen_fail/fail/interface/impl_unqiue_name.rs.disabled create mode 100644 integration_tests/codegen_fail/fail/interface/implementer_non_object_type.rs create mode 100644 integration_tests/codegen_fail/fail/interface/implementer_non_object_type.stderr create mode 100644 integration_tests/codegen_fail/fail/interface/implementers_duplicate_pretty.rs create mode 100644 integration_tests/codegen_fail/fail/interface/implementers_duplicate_pretty.stderr create mode 100644 integration_tests/codegen_fail/fail/interface/implementers_duplicate_ugly.rs create mode 100644 integration_tests/codegen_fail/fail/interface/implementers_duplicate_ugly.stderr create mode 100644 integration_tests/codegen_fail/fail/interface/name_double_underscored.rs create mode 100644 integration_tests/codegen_fail/fail/interface/name_double_underscored.stderr create mode 100644 integration_tests/codegen_fail/fail/interface/no_fields.rs create mode 100644 integration_tests/codegen_fail/fail/interface/no_fields.stderr create mode 100644 integration_tests/codegen_fail/fail/interface/wrong_item.rs create mode 100644 integration_tests/codegen_fail/fail/interface/wrong_item.stderr create mode 100644 integration_tests/juniper_tests/src/codegen/interface_attr.rs delete mode 100644 juniper/src/macros/common.rs create mode 100644 juniper/src/macros/helper/mod.rs rename juniper/src/macros/{subscription_helpers.rs => helper/subscription.rs} (100%) delete mode 100644 juniper/src/macros/interface.rs delete mode 100644 juniper/src/tests/fixtures/starwars/model.rs create mode 100644 juniper_codegen/src/common/gen.rs create mode 100644 juniper_codegen/src/common/mod.rs create mode 100644 juniper_codegen/src/common/parse/attr.rs create mode 100644 juniper_codegen/src/common/parse/downcaster.rs create mode 100644 juniper_codegen/src/common/parse/mod.rs create mode 100644 juniper_codegen/src/graphql_interface/attr.rs create mode 100644 juniper_codegen/src/graphql_interface/mod.rs delete mode 100644 juniper_codegen/src/util/option_ext.rs diff --git a/docs/book/content/advanced/introspection.md b/docs/book/content/advanced/introspection.md index 279b96b1..9bf7e895 100644 --- a/docs/book/content/advanced/introspection.md +++ b/docs/book/content/advanced/introspection.md @@ -30,6 +30,7 @@ result can then be converted to JSON for use with tools and libraries such as [graphql-client](https://github.com/graphql-rust/graphql-client): ```rust +# #![allow(unused_variables)] # extern crate juniper; # extern crate serde_json; use juniper::{EmptyMutation, EmptySubscription, FieldResult, IntrospectionFormat}; diff --git a/docs/book/content/advanced/subscriptions.md b/docs/book/content/advanced/subscriptions.md index 162d57fa..59f14516 100644 --- a/docs/book/content/advanced/subscriptions.md +++ b/docs/book/content/advanced/subscriptions.md @@ -78,6 +78,7 @@ operation returns a [`Future`][Future] with an `Item` value of a `Result &str; +} +# +# fn main() {} +``` + +However, to return values of such [interface][1], we should provide its implementers and the Rust type representing a _boxed value of this trait_. The last one can be represented in two flavors: enum and [trait object][2]. + + +### Enum values (default) + +By default, Juniper generates an enum representing the values of the defined [GraphQL interface][1], and names it straightforwardly, `{Interface}Value`. + +```rust +# extern crate juniper; +use juniper::{graphql_interface, GraphQLObject}; + +#[graphql_interface(for = Human)] // enumerating all implementers is mandatory +trait Character { + fn id(&self) -> &str; +} + +#[derive(GraphQLObject)] +#[graphql(impl = CharacterValue)] // notice enum name, NOT trait name +struct Human { + id: String, +} +#[graphql_interface] // implementing requires macro attribute too, (°o°)! +impl Character for Human { + fn id(&self) -> &str { + &self.id + } +} +# +# fn main() { +let human = Human { id: "human-32".to_owned() }; +// Values type for interface has `From` implementations for all its implementers, +// so we don't need to bother with enum variant names. +let character: CharacterValue = human.into(); +assert_eq!(character.id(), "human-32"); +# } +``` + +Also, enum name can be specified explicitly, if desired. + +```rust +# extern crate juniper; +use juniper::{graphql_interface, GraphQLObject}; + +#[graphql_interface(enum = CharaterInterface, for = Human)] +trait Character { + fn id(&self) -> &str; +} + +#[derive(GraphQLObject)] +#[graphql(impl = CharaterInterface)] +struct Human { + id: String, + home_planet: String, +} +#[graphql_interface] +impl Character for Human { + fn id(&self) -> &str { + &self.id + } +} +# +# fn main() {} +``` + + +### Trait object values + +If, for some reason, we would like to use [trait objects][2] for representing [interface][1] values incorporating dynamic dispatch, that should be specified explicitly in the trait definition. + +Downcasting [trait objects][2] in Rust is not that trivial, that's why macro transforms the trait definition slightly, imposing some additional type parameters under-the-hood. + +> __NOTICE__: +> A __trait has to be [object safe](https://doc.rust-lang.org/stable/reference/items/traits.html#object-safety)__, because schema resolvers will need to return a [trait object][2] to specify a [GraphQL interface][1] behind it. + +```rust +# extern crate juniper; +# extern crate tokio; +use juniper::{graphql_interface, GraphQLObject}; + +// `dyn` argument accepts the name of type alias for the required trait object, +// and macro generates this alias automatically +#[graphql_interface(dyn = DynCharacter, for = Human)] +trait Character { + async fn id(&self) -> &str; // async fields are supported natively +} + +#[derive(GraphQLObject)] +#[graphql(impl = DynCharacter<__S>)] // macro adds `ScalarValue` type parameter to trait, +struct Human { // so it may be specified explicitly when required + id: String, +} +#[graphql_interface(dyn)] // implementing requires to know about dynamic dispatch too +impl Character for Human { + async fn id(&self) -> &str { + &self.id + } +} +# +# #[tokio::main] +# async fn main() { +let human = Human { id: "human-32".to_owned() }; +let character: Box = Box::new(human); +assert_eq!(character.id().await, "human-32"); +# } +``` + + +### Ignoring trait methods + +We may want to omit some trait methods to be assumed as [GraphQL interface][1] fields and ignore them. + +```rust +# extern crate juniper; +use juniper::{graphql_interface, GraphQLObject}; + +#[graphql_interface(for = Human)] +trait Character { + fn id(&self) -> &str; + + #[graphql_interface(ignore)] // or `#[graphql_interface(skip)]`, your choice + fn ignored(&self) -> u32 { 0 } +} + +#[derive(GraphQLObject)] +#[graphql(impl = CharacterValue)] +struct Human { + id: String, +} +#[graphql_interface] // implementing requires macro attribute too, (°o°)! +impl Character for Human { + fn id(&self) -> &str { + &self.id + } +} +# +# fn main() {} +``` + + +### Custom context + +If a context is required in a trait method to resolve a [GraphQL interface][1] field, specify it as an argument. + +```rust +# extern crate juniper; +# use std::collections::HashMap; +use juniper::{graphql_interface, GraphQLObject}; + +struct Database { + humans: HashMap, +} +impl juniper::Context for Database {} + +#[graphql_interface(for = Human)] // look, ma, context type is inferred! \(^o^)/ +trait Character { // while still can be specified via `Context = ...` attribute argument + // If a field argument is named `context` or `ctx`, it's automatically assumed + // as a context argument. + fn id(&self, context: &Database) -> Option<&str>; + + // Otherwise, you may mark it explicitly as a context argument. + fn name(&self, #[graphql_interface(context)] db: &Database) -> Option<&str>; +} + +#[derive(GraphQLObject)] +#[graphql(impl = CharacterValue, Context = Database)] +struct Human { + id: String, + name: String, +} +#[graphql_interface] +impl Character for Human { + fn id(&self, db: &Database) -> Option<&str> { + if db.humans.contains_key(&self.id) { + Some(&self.id) + } else { + None + } + } + + fn name(&self, db: &Database) -> Option<&str> { + if db.humans.contains_key(&self.id) { + Some(&self.name) + } else { + None + } + } +} +# +# fn main() {} +``` + + +### Using executor and explicit generic scalar + +If an executor is required in a trait method to resolve a [GraphQL interface][1] field, specify it as an argument. + +This requires to explicitly parametrize over [`ScalarValue`][3], as [`Executor`][4] does so. + +```rust +# extern crate juniper; +use juniper::{graphql_interface, Executor, GraphQLObject, LookAheadMethods as _, ScalarValue}; + +#[graphql_interface(for = Human, Scalar = S)] // notice specifying scalar as existing type parameter +trait Character { + // If a field argument is named `executor`, it's automatically assumed + // as an executor argument. + async fn id<'a>(&self, executor: &'a Executor<'_, '_, (), S>) -> &'a str + where + S: Send + Sync; // required by `#[async_trait]` transformation ¯\_(ツ)_/¯ + + + // Otherwise, you may mark it explicitly as an executor argument. + async fn name<'b>( + &'b self, + #[graphql_interface(executor)] another: &Executor<'_, '_, (), S>, + ) -> &'b str + where + S: Send + Sync; +} + +#[derive(GraphQLObject)] +#[graphql(impl = CharacterValue<__S>)] +struct Human { + id: String, + name: String, +} +#[graphql_interface(Scalar = S)] +impl Character for Human { + async fn id<'a>(&self, executor: &'a Executor<'_, '_, (), S>) -> &'a str + where + S: Send + Sync, + { + executor.look_ahead().field_name() + } + + async fn name<'b>(&'b self, _: &Executor<'_, '_, (), S>) -> &'b str + where + S: Send + Sync, + { + &self.name + } +} +# +# fn main() {} +``` + + + + + + Traits are maybe the most obvious concept you want to use when building interfaces. But because GraphQL supports downcasting while Rust doesn't, you'll have to manually specify how to convert a trait into a concrete type. This can @@ -63,7 +327,7 @@ juniper::graphql_interface!(<'a> &'a dyn Character: () as "Character" where Scal # fn main() {} ``` -The `instance_resolvers` declaration lists all the implementors of the given +The `instance_resolvers` declaration lists all the implementers of the given interface and how to resolve them. As you can see, you lose a bit of the point with using traits: you need to list @@ -213,3 +477,12 @@ juniper::graphql_interface!(Character: () where Scalar = |&self| { # fn main() {} ``` + + + + + +[1]: https://spec.graphql.org/June2018/#sec-Interfaces +[2]: https://doc.rust-lang.org/reference/types/trait-object.html +[3]: https://docs.rs/juniper/latest/juniper/trait.ScalarValue.html +[4]: https://docs.rs/juniper/latest/juniper/struct.Executor.html \ No newline at end of file diff --git a/docs/book/content/types/objects/complex_fields.md b/docs/book/content/types/objects/complex_fields.md index a33ba047..6abc6a04 100644 --- a/docs/book/content/types/objects/complex_fields.md +++ b/docs/book/content/types/objects/complex_fields.md @@ -10,8 +10,9 @@ example from the last chapter, this is how you would define `Person` using the macro: ```rust +# #![allow(dead_code)] # extern crate juniper; - +# struct Person { name: String, age: i32, diff --git a/docs/book/content/types/unions.md b/docs/book/content/types/unions.md index ab7ef2f3..e8209dab 100644 --- a/docs/book/content/types/unions.md +++ b/docs/book/content/types/unions.md @@ -16,7 +16,7 @@ Most of the time, we just need a trivial and straightforward Rust enum to repres ```rust # extern crate juniper; -# #[macro_use] extern crate derive_more; +# extern crate derive_more; use derive_more::From; use juniper::{GraphQLObject, GraphQLUnion}; @@ -53,7 +53,7 @@ As an example, let's consider the situation where we need to bind some type para ```rust # extern crate juniper; -# #[macro_use] extern crate derive_more; +# extern crate derive_more; # use std::marker::PhantomData; use derive_more::From; use juniper::{GraphQLObject, GraphQLUnion}; @@ -88,6 +88,7 @@ enum Character { If some custom logic is needed to resolve a [GraphQL union][1] variant, you may specify an external function to do so: ```rust +# #![allow(dead_code)] # extern crate juniper; use juniper::{GraphQLObject, GraphQLUnion}; @@ -132,6 +133,7 @@ impl Character { With an external resolver function we can even declare a new [GraphQL union][1] variant where the Rust type is absent in the initial enum definition. The attribute syntax `#[graphql(on VariantType = resolver_fn)]` follows the [GraphQL syntax for dispatching union variants](https://spec.graphql.org/June2018/#example-f8163). ```rust +# #![allow(dead_code)] # extern crate juniper; use juniper::{GraphQLObject, GraphQLUnion}; @@ -289,6 +291,7 @@ impl Character for Droid { If a context is required in a trait method to resolve a [GraphQL union][1] variant, specify it as an argument. ```rust +# #![allow(unused_variables)] # extern crate juniper; # use std::collections::HashMap; use juniper::{graphql_union, GraphQLObject}; @@ -451,6 +454,7 @@ fn get_droid<'db>(ch: &DynCharacter<'_>, ctx: &'db Database) -> Option<&'db Droi By default, `#[derive(GraphQLUnion)]` and `#[graphql_union]` macros generate code, which is generic over a [`ScalarValue`][2] type. This may introduce a problem when at least one of [GraphQL union][1] variants is restricted to a concrete [`ScalarValue`][2] type in its implementation. To resolve such problem, a concrete [`ScalarValue`][2] type should be specified: ```rust +# #![allow(dead_code)] # extern crate juniper; use juniper::{DefaultScalarValue, GraphQLObject, GraphQLUnion}; diff --git a/docs/book/tests/Cargo.toml b/docs/book/tests/Cargo.toml index 120f1a89..4cf997e4 100644 --- a/docs/book/tests/Cargo.toml +++ b/docs/book/tests/Cargo.toml @@ -12,7 +12,7 @@ juniper_subscriptions = { path = "../../../juniper_subscriptions" } derive_more = "0.99.7" futures = "0.3" -tokio = { version = "0.2", features = ["rt-core", "blocking", "stream", "rt-util"] } +tokio = { version = "0.2", features = ["blocking", "macros", "rt-core", "rt-util", "stream"] } iron = "0.5.0" mount = "0.4.0" diff --git a/examples/actix_subscriptions/src/main.rs b/examples/actix_subscriptions/src/main.rs index 152cf22c..2c2c0d7f 100644 --- a/examples/actix_subscriptions/src/main.rs +++ b/examples/actix_subscriptions/src/main.rs @@ -4,7 +4,7 @@ use actix_cors::Cors; use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer}; use juniper::{ - tests::fixtures::starwars::{model::Database, schema::Query}, + tests::fixtures::starwars::schema::{Character as _, Database, Query}, DefaultScalarValue, EmptyMutation, FieldError, RootNode, }; use juniper_actix::{graphql_handler, playground_handler, subscriptions::subscriptions_handler}; @@ -76,11 +76,11 @@ impl Subscription { )) } else { let random_id = rng.gen_range(1000, 1005).to_string(); - let human = context.get_human(&random_id).unwrap(); + let human = context.get_human(&random_id).unwrap().clone(); Ok(RandomHuman { id: human.id().to_owned(), - name: human.name().to_owned(), + name: human.name().unwrap().to_owned(), }) } }); diff --git a/integration_tests/codegen_fail/fail/interface/argument_double_underscored.rs b/integration_tests/codegen_fail/fail/interface/argument_double_underscored.rs new file mode 100644 index 00000000..d7227c38 --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/argument_double_underscored.rs @@ -0,0 +1,19 @@ +use juniper::{graphql_interface, GraphQLObject}; + +#[derive(GraphQLObject)] +#[graphql(impl = CharacterValue)] +pub struct ObjA { + test: String, +} + +#[graphql_interface] +impl Character for ObjA {} + +#[graphql_interface(for = ObjA)] +trait Character { + fn id(&self, __num: i32) -> &str { + "funA" + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/interface/argument_double_underscored.stderr b/integration_tests/codegen_fail/fail/interface/argument_double_underscored.stderr new file mode 100644 index 00000000..f06c385c --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/argument_double_underscored.stderr @@ -0,0 +1,19 @@ +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:14:18 + | +14 | fn id(&self, __num: i32) -> &str { + | ^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Schema + +error[E0412]: cannot find type `CharacterValue` in this scope + --> $DIR/argument_double_underscored.rs:4:18 + | +4 | #[graphql(impl = CharacterValue)] + | ^^^^^^^^^^^^^^ not found in this scope + +error[E0405]: cannot find trait `Character` in this scope + --> $DIR/argument_double_underscored.rs:10:6 + | +10 | impl Character for ObjA {} + | ^^^^^^^^^ not found in this scope diff --git a/integration_tests/codegen_fail/fail/interface/argument_non_input_type.rs b/integration_tests/codegen_fail/fail/interface/argument_non_input_type.rs new file mode 100644 index 00000000..db6ad300 --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/argument_non_input_type.rs @@ -0,0 +1,21 @@ +use juniper::{graphql_interface, GraphQLObject}; + +#[derive(GraphQLObject)] +#[graphql(impl = CharacterValue)] +pub struct ObjA { + test: String, +} + +#[graphql_interface] +impl Character for ObjA { + fn id(&self, obj: Self) -> &str { + "funA" + } +} + +#[graphql_interface(for = ObjA)] +trait Character { + fn id(&self, obj: ObjA) -> &str; +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/interface/argument_non_input_type.stderr b/integration_tests/codegen_fail/fail/interface/argument_non_input_type.stderr new file mode 100644 index 00000000..c4e89fc4 --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/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:16:1 + | +16 | #[graphql_interface(for = ObjA)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IsInputType<__S>` is not implemented for `ObjA` + | + = note: required by `juniper::marker::IsInputType::mark` + = note: this error originates in an attribute macro (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:16:1 + | +16 | #[graphql_interface(for = ObjA)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `FromInputValue<__S>` is not implemented for `ObjA` + | + = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/interface/downcast_method_conflicts_with_external_downcast_fn.rs b/integration_tests/codegen_fail/fail/interface/downcast_method_conflicts_with_external_downcast_fn.rs new file mode 100644 index 00000000..df2e5b85 --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/downcast_method_conflicts_with_external_downcast_fn.rs @@ -0,0 +1,29 @@ +use juniper::{graphql_interface, GraphQLObject}; + +#[derive(GraphQLObject)] +#[graphql(impl = CharacterValue)] +pub struct ObjA { + test: String, +} + +#[graphql_interface] +impl Character for ObjA { + fn id(&self, _: i32) -> &str { + "funA" + } + + fn as_obja(&self) -> Option<&ObjA> { + Some(self) + } +} + +#[graphql_interface(for = ObjA)] +#[graphql_interface(on ObjA = downcast_obja)] +trait Character { + fn id(&self, num: i32) -> &str; + + #[graphql_interface(downcast)] + fn as_obja(&self) -> Option<&ObjA>; +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/interface/downcast_method_conflicts_with_external_downcast_fn.stderr b/integration_tests/codegen_fail/fail/interface/downcast_method_conflicts_with_external_downcast_fn.stderr new file mode 100644 index 00000000..29124c3c --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/downcast_method_conflicts_with_external_downcast_fn.stderr @@ -0,0 +1,20 @@ +error: GraphQL interface trait method `as_obja` conflicts with the external downcast function `downcast_obja` declared on the trait to downcast into the implementer type `ObjA` + --> $DIR/downcast_method_conflicts_with_external_downcast_fn.rs:26:5 + | +26 | fn as_obja(&self) -> Option<&ObjA>; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Interfaces + = note: use `#[graphql_interface(ignore)]` attribute argument to ignore this trait method for interface implementers downcasting + +error[E0412]: cannot find type `CharacterValue` in this scope + --> $DIR/downcast_method_conflicts_with_external_downcast_fn.rs:4:18 + | +4 | #[graphql(impl = CharacterValue)] + | ^^^^^^^^^^^^^^ not found in this scope + +error[E0405]: cannot find trait `Character` in this scope + --> $DIR/downcast_method_conflicts_with_external_downcast_fn.rs:10:6 + | +10 | impl Character for ObjA { + | ^^^^^^^^^ not found in this scope diff --git a/integration_tests/codegen_fail/fail/interface/downcast_method_wrong_input_args.rs b/integration_tests/codegen_fail/fail/interface/downcast_method_wrong_input_args.rs new file mode 100644 index 00000000..c99d6dcf --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/downcast_method_wrong_input_args.rs @@ -0,0 +1,24 @@ +use juniper::{graphql_interface, GraphQLObject}; + +#[graphql_interface(for = Human)] +trait Character { + fn id(&self) -> i32 { + 0 + } + + #[graphql_interface(downcast)] + fn a(&self, ctx: &(), rand: u8) -> Option<&Human> { + None + } +} + +#[derive(GraphQLObject)] +#[graphql(impl = CharacterValue)] +pub struct Human { + id: String, +} + +#[graphql_interface] +impl Character for Human {} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/interface/downcast_method_wrong_input_args.stderr b/integration_tests/codegen_fail/fail/interface/downcast_method_wrong_input_args.stderr new file mode 100644 index 00000000..718a0a25 --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/downcast_method_wrong_input_args.stderr @@ -0,0 +1,19 @@ +error: GraphQL interface expects trait method to accept `&self` only and, optionally, `&Context` + --> $DIR/downcast_method_wrong_input_args.rs:10:10 + | +10 | fn a(&self, ctx: &(), rand: u8) -> Option<&Human> { + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Interfaces + +error[E0412]: cannot find type `CharacterValue` in this scope + --> $DIR/downcast_method_wrong_input_args.rs:16:18 + | +16 | #[graphql(impl = CharacterValue)] + | ^^^^^^^^^^^^^^ not found in this scope + +error[E0405]: cannot find trait `Character` in this scope + --> $DIR/downcast_method_wrong_input_args.rs:22:6 + | +22 | impl Character for Human {} + | ^^^^^^^^^ not found in this scope diff --git a/integration_tests/codegen_fail/fail/interface/downcast_method_wrong_return_type.rs b/integration_tests/codegen_fail/fail/interface/downcast_method_wrong_return_type.rs new file mode 100644 index 00000000..a9949a65 --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/downcast_method_wrong_return_type.rs @@ -0,0 +1,24 @@ +use juniper::{graphql_interface, GraphQLObject}; + +#[graphql_interface(for = Human)] +trait Character { + fn id(&self) -> i32 { + 0 + } + + #[graphql_interface(downcast)] + fn a(&self, ctx: &(), rand: u8) -> &Human { + unimplemented!() + } +} + +#[derive(GraphQLObject)] +#[graphql(impl = CharacterValue)] +pub struct Human { + id: String, +} + +#[graphql_interface] +impl Character for Human {} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/interface/downcast_method_wrong_return_type.stderr b/integration_tests/codegen_fail/fail/interface/downcast_method_wrong_return_type.stderr new file mode 100644 index 00000000..8c459495 --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/downcast_method_wrong_return_type.stderr @@ -0,0 +1,19 @@ +error: GraphQL interface expects trait method return type to be `Option<&ImplementerType>` only + --> $DIR/downcast_method_wrong_return_type.rs:10:40 + | +10 | fn a(&self, ctx: &(), rand: u8) -> &Human { + | ^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Interfaces + +error[E0412]: cannot find type `CharacterValue` in this scope + --> $DIR/downcast_method_wrong_return_type.rs:16:18 + | +16 | #[graphql(impl = CharacterValue)] + | ^^^^^^^^^^^^^^ not found in this scope + +error[E0405]: cannot find trait `Character` in this scope + --> $DIR/downcast_method_wrong_return_type.rs:22:6 + | +22 | impl Character for Human {} + | ^^^^^^^^^ not found in this scope diff --git a/integration_tests/codegen_fail/fail/interface/field_double_underscored.rs b/integration_tests/codegen_fail/fail/interface/field_double_underscored.rs new file mode 100644 index 00000000..43115d32 --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/field_double_underscored.rs @@ -0,0 +1,19 @@ +use juniper::{graphql_interface, GraphQLObject}; + +#[derive(GraphQLObject)] +#[graphql(impl = CharacterValue)] +pub struct ObjA { + test: String, +} + +#[graphql_interface] +impl Character for ObjA {} + +#[graphql_interface(for = ObjA)] +trait Character { + fn __id(&self) -> &str { + "funA" + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/interface/field_double_underscored.stderr b/integration_tests/codegen_fail/fail/interface/field_double_underscored.stderr new file mode 100644 index 00000000..adebb09f --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/field_double_underscored.stderr @@ -0,0 +1,19 @@ +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/field_double_underscored.rs:14:8 + | +14 | fn __id(&self) -> &str { + | ^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Schema + +error[E0412]: cannot find type `CharacterValue` in this scope + --> $DIR/field_double_underscored.rs:4:18 + | +4 | #[graphql(impl = CharacterValue)] + | ^^^^^^^^^^^^^^ not found in this scope + +error[E0405]: cannot find trait `Character` in this scope + --> $DIR/field_double_underscored.rs:10:6 + | +10 | impl Character for ObjA {} + | ^^^^^^^^^ not found in this scope diff --git a/integration_tests/codegen_fail/fail/interface/field_non_output_return_type.rs b/integration_tests/codegen_fail/fail/interface/field_non_output_return_type.rs new file mode 100644 index 00000000..a860ace3 --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/field_non_output_return_type.rs @@ -0,0 +1,24 @@ +use juniper::{graphql_interface, GraphQLInputObject, GraphQLObject}; + +#[derive(GraphQLInputObject)] +pub struct ObjB { + id: i32, +} + +#[derive(GraphQLObject)] +#[graphql(impl = CharacterValue)] +pub struct ObjA { + test: String, +} + +#[graphql_interface] +impl Character for ObjA {} + +#[graphql_interface(for = ObjA)] +trait Character { + fn id(&self) -> ObjB { + ObjB { id: 34 } + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/interface/field_non_output_return_type.stderr b/integration_tests/codegen_fail/fail/interface/field_non_output_return_type.stderr new file mode 100644 index 00000000..73b266d3 --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/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:17:1 + | +17 | #[graphql_interface(for = ObjA)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IsOutputType<__S>` is not implemented for `ObjB` + | + = note: required by `juniper::marker::IsOutputType::mark` + = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/interface/fields_duplicate.rs b/integration_tests/codegen_fail/fail/interface/fields_duplicate.rs new file mode 100644 index 00000000..f3ae4673 --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/fields_duplicate.rs @@ -0,0 +1,24 @@ +use juniper::{graphql_interface, GraphQLObject}; + +#[derive(GraphQLObject)] +#[graphql(impl = CharacterValue)] +pub struct ObjA { + test: String, +} + +#[graphql_interface] +impl Character for ObjA {} + +#[graphql_interface(for = ObjA)] +trait Character { + fn id(&self) -> &str { + "funA" + } + + #[graphql_interface(name = "id")] + fn id2(&self) -> &str { + "funB" + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/interface/fields_duplicate.stderr b/integration_tests/codegen_fail/fail/interface/fields_duplicate.stderr new file mode 100644 index 00000000..db133386 --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/fields_duplicate.stderr @@ -0,0 +1,25 @@ +error: GraphQL interface must have a different name for each field + --> $DIR/fields_duplicate.rs:13:1 + | +13 | / trait Character { +14 | | fn id(&self) -> &str { +15 | | "funA" +16 | | } +... | +21 | | } +22 | | } + | |_^ + | + = note: https://spec.graphql.org/June2018/#sec-Interfaces + +error[E0412]: cannot find type `CharacterValue` in this scope + --> $DIR/fields_duplicate.rs:4:18 + | +4 | #[graphql(impl = CharacterValue)] + | ^^^^^^^^^^^^^^ not found in this scope + +error[E0405]: cannot find trait `Character` in this scope + --> $DIR/fields_duplicate.rs:10:6 + | +10 | impl Character for ObjA {} + | ^^^^^^^^^ not found in this scope diff --git a/integration_tests/codegen_fail/fail/interface/impl_argument_no_object.rs b/integration_tests/codegen_fail/fail/interface/impl_argument_no_object.rs deleted file mode 100644 index 1ab0e6e1..00000000 --- a/integration_tests/codegen_fail/fail/interface/impl_argument_no_object.rs +++ /dev/null @@ -1,23 +0,0 @@ -#[derive(juniper::GraphQLObject)] -#[graphql(scalar = juniper::DefaultScalarValue)] -pub struct ObjA { - test: String, -} - -enum Character { - A(ObjA), -} - -juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| { - field id(__test: ObjA) -> &str { - match *self { - Character::A(_) => "funA", - } - } - - instance_resolvers: |_| { - &ObjA => match *self { Character::A(ref h) => Some(h) }, - } -}); - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/interface/impl_argument_no_object.stderr b/integration_tests/codegen_fail/fail/interface/impl_argument_no_object.stderr deleted file mode 100644 index 10bf97ff..00000000 --- a/integration_tests/codegen_fail/fail/interface/impl_argument_no_object.stderr +++ /dev/null @@ -1,27 +0,0 @@ -error[E0277]: the trait bound `ObjA: FromInputValue` is not satisfied - --> $DIR/impl_argument_no_object.rs:11:1 - | -11 | / juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| { -12 | | field id(__test: ObjA) -> &str { -13 | | match *self { -14 | | Character::A(_) => "funA", -... | -20 | | } -21 | | }); - | |___^ the trait `FromInputValue` is not implemented for `ObjA` - | - = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0277]: the trait bound `ObjA: FromInputValue` is not satisfied - --> $DIR/impl_argument_no_object.rs:11:1 - | -11 | / juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| { -12 | | field id(__test: ObjA) -> &str { -13 | | match *self { -14 | | Character::A(_) => "funA", -... | -20 | | } -21 | | }); - | |___^ the trait `FromInputValue` is not implemented for `ObjA` - | - = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/interface/impl_argument_no_underscore.rs.disabled b/integration_tests/codegen_fail/fail/interface/impl_argument_no_underscore.rs.disabled deleted file mode 100644 index 65f8117d..00000000 --- a/integration_tests/codegen_fail/fail/interface/impl_argument_no_underscore.rs.disabled +++ /dev/null @@ -1,23 +0,0 @@ -#[derive(juniper::GraphQLObject)] -#[graphql(scalar = juniper::DefaultScalarValue)] -pub struct ObjA { - test: String, -} - -enum Character { - A(ObjA), -} - -juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| { - field id(__test: String) -> &str { - match *self { - Character::A(_) => "funA", - } - } - - instance_resolvers: |_| { - &ObjA => match *self { Character::A(ref h) => Some(h) }, - } -}); - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/interface/impl_no_fields.rs.disabled b/integration_tests/codegen_fail/fail/interface/impl_no_fields.rs.disabled deleted file mode 100644 index 4af462fa..00000000 --- a/integration_tests/codegen_fail/fail/interface/impl_no_fields.rs.disabled +++ /dev/null @@ -1,12 +0,0 @@ -enum Character {} - -juniper::graphql_interface!(Character: () where Scalar = |&self| { - field id() -> &str { - match *self { - } - } - - instance_resolvers: |_| {} -}); - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/interface/impl_no_input_object.rs.disabled b/integration_tests/codegen_fail/fail/interface/impl_no_input_object.rs.disabled deleted file mode 100644 index e3075f8a..00000000 --- a/integration_tests/codegen_fail/fail/interface/impl_no_input_object.rs.disabled +++ /dev/null @@ -1,23 +0,0 @@ -#[derive(juniper::GraphQLInputObject)] -#[graphql(scalar = juniper::DefaultScalarValue)] -pub struct ObjA { - test: String, -} - -enum Character { - A(ObjA), -} - -juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| { - field id() -> &str { - match *self { - Character::A(_) => "funA", - } - } - - instance_resolvers: |_| { - &ObjA => match *self { Character::A(ref h) => Some(h) }, - } -}); - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/interface/impl_no_underscore.rs.disabled b/integration_tests/codegen_fail/fail/interface/impl_no_underscore.rs.disabled deleted file mode 100644 index 1ff44b94..00000000 --- a/integration_tests/codegen_fail/fail/interface/impl_no_underscore.rs.disabled +++ /dev/null @@ -1,23 +0,0 @@ -#[derive(juniper::GraphQLObject)] -#[graphql(scalar = juniper::DefaultScalarValue)] -pub struct ObjA { - test: String, -} - -enum Character { - A(ObjA), -} - -juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| { - field __id() -> &str { - match *self { - Character::A(_) => "funA", - } - } - - instance_resolvers: |_| { - &ObjA => match *self { Character::A(ref h) => Some(h) }, - } -}); - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/interface/impl_unqiue_name.rs.disabled b/integration_tests/codegen_fail/fail/interface/impl_unqiue_name.rs.disabled deleted file mode 100644 index c0b88b4b..00000000 --- a/integration_tests/codegen_fail/fail/interface/impl_unqiue_name.rs.disabled +++ /dev/null @@ -1,29 +0,0 @@ -#[derive(juniper::GraphQLObject)] -#[graphql(scalar = juniper::DefaultScalarValue)] -pub struct ObjA { - test: String, -} - -enum Character { - A(ObjA), -} - -juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| { - field id() -> &str { - match *self { - Character::A(_) => "funA", - } - } - - field id() -> &str { - match *self { - Character::A(_) => "funA", - } - } - - instance_resolvers: |_| { - &ObjA => match *self { Character::A(ref h) => Some(h) }, - } -}); - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/interface/implementer_non_object_type.rs b/integration_tests/codegen_fail/fail/interface/implementer_non_object_type.rs new file mode 100644 index 00000000..0aba2508 --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/implementer_non_object_type.rs @@ -0,0 +1,20 @@ +use juniper::{graphql_interface, GraphQLInputObject}; + +#[derive(GraphQLInputObject)] +pub struct ObjA { + test: String, +} + +#[graphql_interface] +impl Character for ObjA { + fn id(&self) -> &str { + "funA" + } +} + +#[graphql_interface(for = ObjA)] +trait Character { + fn id(&self) -> &str; +} + +fn main() {} 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 new file mode 100644 index 00000000..2de9329c --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/implementer_non_object_type.stderr @@ -0,0 +1,17 @@ +error[E0277]: the trait bound `ObjA: GraphQLObjectType<__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` + | + = note: required by `juniper::marker::GraphQLObjectType::mark` + = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `ObjA: IsOutputType<__S>` is not satisfied + --> $DIR/implementer_non_object_type.rs:15:1 + | +15 | #[graphql_interface(for = ObjA)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IsOutputType<__S>` is not implemented for `ObjA` + | + = note: required by `juniper::marker::IsOutputType::mark` + = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/interface/implementers_duplicate_pretty.rs b/integration_tests/codegen_fail/fail/interface/implementers_duplicate_pretty.rs new file mode 100644 index 00000000..fbdc8f12 --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/implementers_duplicate_pretty.rs @@ -0,0 +1,21 @@ +use juniper::{graphql_interface, GraphQLObject}; + +#[derive(GraphQLObject)] +#[graphql(impl = CharacterValue)] +pub struct ObjA { + test: String, +} + +#[graphql_interface] +impl Character for ObjA { + fn id(&self) -> &str { + "funA" + } +} + +#[graphql_interface(for = [ObjA, ObjA])] +trait Character { + fn id(&self) -> &str; +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/interface/implementers_duplicate_pretty.stderr b/integration_tests/codegen_fail/fail/interface/implementers_duplicate_pretty.stderr new file mode 100644 index 00000000..40db273e --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/implementers_duplicate_pretty.stderr @@ -0,0 +1,17 @@ +error: duplicated attribute argument found + --> $DIR/implementers_duplicate_pretty.rs:16:34 + | +16 | #[graphql_interface(for = [ObjA, ObjA])] + | ^^^^ + +error[E0412]: cannot find type `CharacterValue` in this scope + --> $DIR/implementers_duplicate_pretty.rs:4:18 + | +4 | #[graphql(impl = CharacterValue)] + | ^^^^^^^^^^^^^^ not found in this scope + +error[E0405]: cannot find trait `Character` in this scope + --> $DIR/implementers_duplicate_pretty.rs:10:6 + | +10 | impl Character for ObjA { + | ^^^^^^^^^ not found in this scope diff --git a/integration_tests/codegen_fail/fail/interface/implementers_duplicate_ugly.rs b/integration_tests/codegen_fail/fail/interface/implementers_duplicate_ugly.rs new file mode 100644 index 00000000..1acb56c0 --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/implementers_duplicate_ugly.rs @@ -0,0 +1,23 @@ +use juniper::{graphql_interface, GraphQLObject}; + +#[derive(GraphQLObject)] +#[graphql(impl = CharacterValue)] +pub struct ObjA { + test: String, +} + +type ObjAlias = ObjA; + +#[graphql_interface] +impl Character for ObjA { + fn id(&self) -> &str { + "funA" + } +} + +#[graphql_interface(for = [ObjA, ObjAlias])] +trait Character { + fn id(&self) -> &str; +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/interface/implementers_duplicate_ugly.stderr b/integration_tests/codegen_fail/fail/interface/implementers_duplicate_ugly.stderr new file mode 100644 index 00000000..2a62e2ea --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/implementers_duplicate_ugly.stderr @@ -0,0 +1,21 @@ +error[E0119]: conflicting implementations of trait `>::mark::_::{closure#0}::MutuallyExclusive` for type `ObjA`: + --> $DIR/implementers_duplicate_ugly.rs:18:1 + | +18 | #[graphql_interface(for = [ObjA, ObjAlias])] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | | + | first implementation here + | conflicting implementation for `ObjA` + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0119]: conflicting implementations of trait `std::convert::From` for type `CharacterValue`: + --> $DIR/implementers_duplicate_ugly.rs:18:1 + | +18 | #[graphql_interface(for = [ObjA, ObjAlias])] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | | + | first implementation here + | conflicting implementation for `CharacterValue` + | + = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/interface/name_double_underscored.rs b/integration_tests/codegen_fail/fail/interface/name_double_underscored.rs new file mode 100644 index 00000000..b233119f --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/name_double_underscored.rs @@ -0,0 +1,10 @@ +use juniper::graphql_interface; + +#[graphql_interface] +trait __Character { + fn id(&self) -> &str { + "funA" + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/interface/name_double_underscored.stderr b/integration_tests/codegen_fail/fail/interface/name_double_underscored.stderr new file mode 100644 index 00000000..de8c6715 --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/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:4:7 + | +4 | trait __Character { + | ^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Schema diff --git a/integration_tests/codegen_fail/fail/interface/no_fields.rs b/integration_tests/codegen_fail/fail/interface/no_fields.rs new file mode 100644 index 00000000..11596035 --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/no_fields.rs @@ -0,0 +1,15 @@ +use juniper::{graphql_interface, GraphQLObject}; + +#[derive(GraphQLObject)] +#[graphql(impl = CharacterValue)] +pub struct ObjA { + test: String, +} + +#[graphql_interface] +impl Character for ObjA {} + +#[graphql_interface(for = ObjA)] +trait Character {} + +fn main() {} \ No newline at end of file diff --git a/integration_tests/codegen_fail/fail/interface/no_fields.stderr b/integration_tests/codegen_fail/fail/interface/no_fields.stderr new file mode 100644 index 00000000..367a5e9b --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/no_fields.stderr @@ -0,0 +1,19 @@ +error: GraphQL interface must have at least one field + --> $DIR/no_fields.rs:13:1 + | +13 | trait Character {} + | ^^^^^^^^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Interfaces + +error[E0412]: cannot find type `CharacterValue` in this scope + --> $DIR/no_fields.rs:4:18 + | +4 | #[graphql(impl = CharacterValue)] + | ^^^^^^^^^^^^^^ not found in this scope + +error[E0405]: cannot find trait `Character` in this scope + --> $DIR/no_fields.rs:10:6 + | +10 | impl Character for ObjA {} + | ^^^^^^^^^ not found in this scope diff --git a/integration_tests/codegen_fail/fail/interface/wrong_item.rs b/integration_tests/codegen_fail/fail/interface/wrong_item.rs new file mode 100644 index 00000000..9b37b942 --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/wrong_item.rs @@ -0,0 +1,11 @@ +use juniper::{graphql_interface, GraphQLObject}; + +#[derive(GraphQLObject)] +pub struct ObjA { + test: String, +} + +#[graphql_interface(for = ObjA)] +enum Character {} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/interface/wrong_item.stderr b/integration_tests/codegen_fail/fail/interface/wrong_item.stderr new file mode 100644 index 00000000..8bb40afa --- /dev/null +++ b/integration_tests/codegen_fail/fail/interface/wrong_item.stderr @@ -0,0 +1,7 @@ +error: #[graphql_interface] attribute is applicable to trait definitions and trait implementations only + --> $DIR/wrong_item.rs:8:1 + | +8 | #[graphql_interface(for = ObjA)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/union/enum_same_type_ugly.stderr b/integration_tests/codegen_fail/fail/union/enum_same_type_ugly.stderr index f8c1a9f0..f703e72b 100644 --- a/integration_tests/codegen_fail/fail/union/enum_same_type_ugly.stderr +++ b/integration_tests/codegen_fail/fail/union/enum_same_type_ugly.stderr @@ -1,4 +1,4 @@ -error[E0119]: conflicting implementations of trait `>::mark::_::{{closure}}#0::MutuallyExclusive` for type `std::string::String`: +error[E0119]: conflicting implementations of trait `>::mark::_::{closure#0}::MutuallyExclusive` for type `std::string::String`: --> $DIR/enum_same_type_ugly.rs:3:10 | 3 | #[derive(GraphQLUnion)] diff --git a/integration_tests/codegen_fail/fail/union/struct_same_type_pretty.stderr b/integration_tests/codegen_fail/fail/union/struct_same_type_pretty.stderr index fca94bfb..9e3152bd 100644 --- a/integration_tests/codegen_fail/fail/union/struct_same_type_pretty.stderr +++ b/integration_tests/codegen_fail/fail/union/struct_same_type_pretty.stderr @@ -1,4 +1,4 @@ -error: duplicated attribute +error: duplicated attribute argument found --> $DIR/struct_same_type_pretty.rs:5:14 | 5 | #[graphql(on i32 = Character::b)] diff --git a/integration_tests/codegen_fail/fail/union/struct_same_type_ugly.stderr b/integration_tests/codegen_fail/fail/union/struct_same_type_ugly.stderr index 08d35082..81bf349a 100644 --- a/integration_tests/codegen_fail/fail/union/struct_same_type_ugly.stderr +++ b/integration_tests/codegen_fail/fail/union/struct_same_type_ugly.stderr @@ -1,4 +1,4 @@ -error[E0119]: conflicting implementations of trait `>::mark::_::{{closure}}#0::MutuallyExclusive` for type `std::string::String`: +error[E0119]: conflicting implementations of trait `>::mark::_::{closure#0}::MutuallyExclusive` for type `std::string::String`: --> $DIR/struct_same_type_ugly.rs:3:10 | 3 | #[derive(GraphQLUnion)] diff --git a/integration_tests/codegen_fail/fail/union/trait_same_type_ugly.stderr b/integration_tests/codegen_fail/fail/union/trait_same_type_ugly.stderr index f1cb6485..979c7142 100644 --- a/integration_tests/codegen_fail/fail/union/trait_same_type_ugly.stderr +++ b/integration_tests/codegen_fail/fail/union/trait_same_type_ugly.stderr @@ -1,4 +1,4 @@ -error[E0119]: conflicting implementations of trait `<(dyn Character + std::marker::Send + std::marker::Sync + '__obj) as juniper::GraphQLUnion<__S>>::mark::_::{{closure}}#0::MutuallyExclusive` for type `std::string::String`: +error[E0119]: conflicting implementations of trait `<(dyn Character + std::marker::Send + std::marker::Sync + '__obj) as juniper::GraphQLUnion<__S>>::mark::_::{closure#0}::MutuallyExclusive` for type `std::string::String`: --> $DIR/trait_same_type_ugly.rs:3:1 | 3 | #[graphql_union] diff --git a/integration_tests/juniper_tests/Cargo.toml b/integration_tests/juniper_tests/Cargo.toml index 89357147..7fedd6d8 100644 --- a/integration_tests/juniper_tests/Cargo.toml +++ b/integration_tests/juniper_tests/Cargo.toml @@ -10,6 +10,7 @@ futures = "0.3.1" juniper = { path = "../../juniper" } [dev-dependencies] +async-trait = "0.1.39" serde_json = { version = "1" } fnv = "1.0.3" tokio = { version = "0.2", features = ["rt-core", "time", "macros"] } \ No newline at end of file diff --git a/integration_tests/juniper_tests/src/codegen/interface_attr.rs b/integration_tests/juniper_tests/src/codegen/interface_attr.rs new file mode 100644 index 00000000..3cf4f198 --- /dev/null +++ b/integration_tests/juniper_tests/src/codegen/interface_attr.rs @@ -0,0 +1,5259 @@ +//! Tests for `#[graphql_interface]` macro. + +use juniper::{ + execute, graphql_interface, graphql_object, graphql_value, DefaultScalarValue, EmptyMutation, + EmptySubscription, Executor, FieldError, FieldResult, GraphQLObject, GraphQLType, + IntoFieldError, RootNode, ScalarValue, Variables, +}; + +fn schema<'q, C, S, Q>(query_root: Q) -> RootNode<'q, Q, EmptyMutation, EmptySubscription, S> +where + Q: GraphQLType + 'q, + S: ScalarValue + 'q, +{ + RootNode::new( + query_root, + EmptyMutation::::new(), + EmptySubscription::::new(), + ) +} + +mod no_implers { + use super::*; + + #[graphql_interface] + trait Character { + fn id(&self) -> &str; + } + + #[graphql_interface(dyn = DynHero)] + trait Hero { + fn info(&self) -> &str; + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> CharacterValue { + unimplemented!() + } + + fn hero(&self) -> Box> { + unimplemented!() + } + } + + #[tokio::test] + async fn is_graphql_interface() { + let schema = schema(QueryRoot); + + for interface in &["Character", "Hero"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + kind + }} + }}"#, + interface, + ); + + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"kind": "INTERFACE"}}), vec![])), + ); + } + } + + #[tokio::test] + async fn uses_trait_name() { + let schema = schema(QueryRoot); + + for interface in &["Character", "Hero"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + name + }} + }}"#, + interface, + ); + + let expected_name: &str = *interface; + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": expected_name}}), vec![])), + ); + } + } + + #[tokio::test] + async fn has_no_description() { + let schema = schema(QueryRoot); + + for interface in &["Character", "Hero"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + description + }} + }}"#, + interface, + ); + + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"description": None}}), vec![])), + ); + } + } +} + +mod trivial { + use super::*; + + #[graphql_interface(for = [Human, Droid])] + trait Character { + fn id(&self) -> &str; + } + + #[graphql_interface(dyn = DynHero, for = [Human, Droid])] + trait Hero { + fn info(&self) -> &str; + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero<__S>])] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface] + impl Character for Human { + fn id(&self) -> &str { + &self.id + } + } + + #[graphql_interface(dyn)] + impl Hero for Human { + fn info(&self) -> &str { + &self.home_planet + } + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero<__S>])] + struct Droid { + id: String, + primary_function: String, + } + + #[graphql_interface] + impl Character for Droid { + fn id(&self) -> &str { + &self.id + } + } + + #[graphql_interface(dyn)] + impl Hero for Droid { + fn info(&self) -> &str { + &self.primary_function + } + } + + #[derive(Clone, Copy)] + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + 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(), + 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![])), + ); + } + } + + #[tokio::test] + async fn is_graphql_interface() { + let schema = schema(QueryRoot::Human); + + for interface in &["Character", "Hero"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + kind + }} + }}"#, + interface, + ); + + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"kind": "INTERFACE"}}), vec![])), + ); + } + } + + #[tokio::test] + async fn registers_all_implementers() { + let schema = schema(QueryRoot::Human); + + for interface in &["Character", "Hero"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + possibleTypes {{ + kind + name + }} + }} + }}"#, + interface, + ); + + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"possibleTypes": [ + {"kind": "OBJECT", "name": "Droid"}, + {"kind": "OBJECT", "name": "Human"}, + ]}}), + vec![], + )), + ); + } + } + + #[tokio::test] + async fn registers_itself_in_implementers() { + let schema = schema(QueryRoot::Human); + + for object in &["Human", "Droid"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + interfaces {{ + kind + name + }} + }} + }}"#, + object, + ); + + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"interfaces": [ + {"kind": "INTERFACE", "name": "Character"}, + {"kind": "INTERFACE", "name": "Hero"}, + ]}}), + vec![], + )), + ); + } + } + + #[tokio::test] + async fn uses_trait_name() { + let schema = schema(QueryRoot::Human); + + for interface in &["Character", "Hero"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + name + }} + }}"#, + interface, + ); + + let expected_name: &str = *interface; + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": expected_name}}), vec![])), + ); + } + } + + #[tokio::test] + async fn has_no_description() { + let schema = schema(QueryRoot::Human); + + for interface in &["Character", "Hero"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + description + }} + }}"#, + interface, + ); + + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"description": None}}), vec![])), + ); + } + } +} + +mod explicit_alias { + use super::*; + + #[graphql_interface(enum = CharacterEnum, for = [Human, Droid])] + trait Character { + fn id(&self) -> &str; + } + + #[derive(GraphQLObject)] + #[graphql(impl = CharacterEnum)] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface] + impl Character for Human { + fn id(&self) -> &str { + &self.id + } + } + + #[derive(GraphQLObject)] + #[graphql(impl = CharacterEnum)] + struct Droid { + id: String, + primary_function: String, + } + + #[graphql_interface] + impl Character for Droid { + fn id(&self) -> &str { + &self.id + } + } + + #[derive(Clone, Copy)] + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> CharacterEnum { + 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(), + } + } + } + + #[tokio::test] + async fn 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 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 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 is_graphql_interface() { + const DOC: &str = r#"{ + __type(name: "Character") { + kind + } + }"#; + + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"kind": "INTERFACE"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_trait_name() { + 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![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Character") { + description + } + }"#; + + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"description": None}}), vec![])), + ); + } +} + +mod trivial_async { + use super::*; + + #[graphql_interface(for = [Human, Droid])] + trait Character { + async fn id(&self) -> &str; + } + + #[graphql_interface(dyn = DynHero, for = [Human, Droid])] + trait Hero { + async fn info(&self) -> &str; + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero<__S>])] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface] + impl Character for Human { + async fn id(&self) -> &str { + &self.id + } + } + + #[graphql_interface(dyn)] + 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] + impl Character for Droid { + async fn id(&self) -> &str { + &self.id + } + } + + #[graphql_interface(dyn)] + impl Hero for Droid { + async fn info(&self) -> &str { + &self.primary_function + } + } + + #[derive(Clone, Copy)] + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + 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(), + 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![])), + ); + } + } + + #[tokio::test] + async fn is_graphql_interface() { + let schema = schema(QueryRoot::Human); + + for interface in &["Character", "Hero"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + kind + }} + }}"#, + interface, + ); + + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"kind": "INTERFACE"}}), vec![])), + ); + } + } + + #[tokio::test] + async fn registers_all_implementers() { + let schema = schema(QueryRoot::Human); + + for interface in &["Character", "Hero"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + possibleTypes {{ + kind + name + }} + }} + }}"#, + interface, + ); + + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"possibleTypes": [ + {"kind": "OBJECT", "name": "Droid"}, + {"kind": "OBJECT", "name": "Human"}, + ]}}), + vec![], + )), + ); + } + } + + #[tokio::test] + async fn registers_itself_in_implementers() { + let schema = schema(QueryRoot::Human); + + for object in &["Human", "Droid"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + interfaces {{ + kind + name + }} + }} + }}"#, + object, + ); + + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"interfaces": [ + {"kind": "INTERFACE", "name": "Character"}, + {"kind": "INTERFACE", "name": "Hero"}, + ]}}), + vec![], + )), + ); + } + } + + #[tokio::test] + async fn uses_trait_name() { + let schema = schema(QueryRoot::Human); + + for interface in &["Character", "Hero"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + name + }} + }}"#, + interface, + ); + + let expected_name: &str = *interface; + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": expected_name}}), vec![])), + ); + } + } + + #[tokio::test] + async fn has_no_description() { + let schema = schema(QueryRoot::Human); + + for interface in &["Character", "Hero"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + description + }} + }}"#, + interface, + ); + + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"description": None}}), vec![])), + ); + } + } +} + +mod explicit_async { + use super::*; + + #[graphql_interface(for = [Human, Droid])] + trait Character { + fn id(&self) -> &str; + + async fn info(&self) -> String { + "None available".to_owned() + } + } + + #[graphql_interface(dyn = DynHero, for = [Human, Droid])] + trait Hero { + async fn id(&self) -> &str { + "Non-identified" + } + + fn info(&self) -> &str; + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero<__S>])] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface] + impl Character for Human { + fn id(&self) -> &str { + &self.id + } + + async fn info(&self) -> String { + format!("Home planet is {}", &self.home_planet) + } + } + + #[graphql_interface(async, dyn)] + impl Hero for Human { + fn info(&self) -> &str { + &self.home_planet + } + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero<__S>])] + struct Droid { + id: String, + primary_function: String, + } + + #[graphql_interface(async)] + impl Character for Droid { + fn id(&self) -> &str { + &self.id + } + } + + #[graphql_interface(dyn)] + impl Hero for Droid { + async fn id(&self) -> &str { + &self.id + } + + fn info(&self) -> &str { + &self.primary_function + } + } + + #[derive(Clone, Copy)] + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + 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(), + 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_fields() { + const DOC: &str = r#"{ + character { + id + info + } + }"#; + + for (root, expected_id, expected_info) in &[ + (QueryRoot::Human, "human-32", "Home planet is earth"), + (QueryRoot::Droid, "droid-99", "None available"), + ] { + let schema = schema(*root); + + let expected_id: &str = *expected_id; + let expected_info: &str = *expected_info; + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": { + "id": expected_id, + "info": expected_info, + }}), + vec![], + )), + ); + } + } + + #[tokio::test] + async fn dyn_resolves_fields() { + const DOC: &str = r#"{ + hero { + id + info + } + }"#; + + for (root, expected_id, expected_info) in &[ + (QueryRoot::Human, "Non-identified", "earth"), + (QueryRoot::Droid, "droid-99", "run"), + ] { + let schema = schema(*root); + + let expected_id: &str = *expected_id; + let expected_info: &str = *expected_info; + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"hero": { + "id": expected_id, + "info": expected_info, + }}), + vec![], + )), + ); + } + } +} + +mod fallible_field { + use super::*; + + struct CustomError; + + impl IntoFieldError for CustomError { + fn into_field_error(self) -> FieldError { + juniper::FieldError::new("Whatever", graphql_value!({"code": "some"})) + } + } + + #[graphql_interface(for = [Human, Droid])] + trait Character { + fn id(&self) -> Result<&str, CustomError>; + } + + #[graphql_interface(dyn = DynHero, for = [Human, Droid])] + trait Hero { + fn info(&self) -> Result<&str, CustomError>; + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero<__S>])] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface] + impl Character for Human { + fn id(&self) -> Result<&str, CustomError> { + Ok(&self.id) + } + } + + #[graphql_interface(dyn)] + impl Hero for Human { + fn info(&self) -> Result<&str, CustomError> { + Ok(&self.home_planet) + } + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero<__S>])] + struct Droid { + id: String, + primary_function: String, + } + + #[graphql_interface] + impl Character for Droid { + fn id(&self) -> Result<&str, CustomError> { + Ok(&self.id) + } + } + + #[graphql_interface(dyn)] + impl Hero for Droid { + fn info(&self) -> Result<&str, CustomError> { + Ok(&self.primary_function) + } + } + + #[derive(Clone, Copy)] + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + 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(), + 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![])), + ); + } + } + + #[tokio::test] + async fn has_correct_graphql_type() { + let schema = schema(QueryRoot::Human); + + for (interface, field) in &[("Character", "id"), ("Hero", "info")] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + name + kind + fields {{ + name + type {{ + kind + ofType {{ + name + }} + }} + }} + }} + }}"#, + interface, + ); + + let expected_name: &str = *interface; + let expected_field_name: &str = *field; + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": { + "name": expected_name, + "kind": "INTERFACE", + "fields": [{ + "name": expected_field_name, + "type": {"kind": "NON_NULL", "ofType": {"name": "String"}}, + }] + }}), + vec![], + )), + ); + } + } +} + +mod generic { + use super::*; + + #[graphql_interface(for = [Human, Droid])] + trait Character { + fn id(&self) -> &str; + } + + #[graphql_interface(dyn = DynHero, for = [Human, Droid])] + trait Hero { + fn info(&self) -> &str; + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero])] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface] + impl Character for Human { + fn id(&self) -> &str { + &self.id + } + } + + #[graphql_interface(dyn)] + impl Hero for Human { + fn info(&self) -> &str { + &self.home_planet + } + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue<(), u8>, DynHero])] + struct Droid { + id: String, + primary_function: String, + } + + #[graphql_interface] + impl Character for Droid { + fn id(&self) -> &str { + &self.id + } + } + + #[graphql_interface(dyn)] + impl Hero for Droid { + fn info(&self) -> &str { + &self.primary_function + } + } + + #[derive(Clone, Copy)] + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + 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(), + 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![])), + ); + } + } + + #[tokio::test] + async fn uses_trait_name_without_type_params() { + for interface in &["Character", "Hero"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + name + }} + }}"#, + interface, + ); + + let schema = schema(QueryRoot::Human); + + let expected_name: &str = *interface; + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": expected_name}}), vec![])), + ); + } + } +} + +mod generic_async { + use super::*; + + #[graphql_interface(for = [Human, Droid])] + trait Character { + async fn id(&self) -> &str; + } + + #[graphql_interface(dyn = DynHero, for = [Human, Droid])] + trait Hero { + async fn info(&self) -> &str; + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero])] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface] + impl Character for Human { + async fn id(&self) -> &str { + &self.id + } + } + + #[graphql_interface(dyn)] + impl Hero for Human { + async fn info(&self) -> &str { + &self.home_planet + } + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue<(), u8>, DynHero])] + struct Droid { + id: String, + primary_function: String, + } + + #[graphql_interface] + impl Character for Droid { + async fn id(&self) -> &str { + &self.id + } + } + + #[graphql_interface(dyn)] + impl Hero for Droid { + async fn info(&self) -> &str { + &self.primary_function + } + } + + #[derive(Clone, Copy)] + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + 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(), + 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![])), + ); + } + } + + #[tokio::test] + async fn uses_trait_name_without_type_params() { + for interface in &["Character", "Hero"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + name + }} + }}"#, + interface, + ); + + let schema = schema(QueryRoot::Human); + + let expected_name: &str = *interface; + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": expected_name}}), vec![])), + ); + } + } +} + +mod generic_lifetime_async { + use super::*; + + #[graphql_interface(for = [Human, Droid])] + trait Character<'me, A> { + async fn id<'a>(&'a self) -> &'a str; + } + + #[graphql_interface(dyn = DynHero, for = [Human, Droid])] + trait Hero<'me, A> { + async fn info(&self) -> &str; + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue<()>, DynHero<(), __S>])] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface] + impl<'me, A> Character<'me, A> for Human { + async fn id<'a>(&'a self) -> &'a str { + &self.id + } + } + + #[graphql_interface(dyn)] + impl<'me, A> Hero<'me, A> 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] + impl<'me, A> Character<'me, A> for Droid { + async fn id<'a>(&'a self) -> &'a str { + &self.id + } + } + + #[graphql_interface(dyn)] + impl<'me, A> Hero<'me, A> for Droid { + async fn info(&self) -> &str { + &self.primary_function + } + } + + #[derive(Clone, Copy)] + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + 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(), + 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![])), + ); + } + } + + #[tokio::test] + async fn uses_trait_name_without_type_params() { + for interface in &["Character", "Hero"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + name + }} + }}"#, + interface, + ); + + let schema = schema(QueryRoot::Human); + + let expected_name: &str = *interface; + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": expected_name}}), vec![])), + ); + } + } +} + +mod argument { + use super::*; + + #[graphql_interface(for = Human)] + trait Character { + fn id_wide(&self, is_number: bool) -> &str; + } + + #[graphql_interface(dyn = DynHero, for = Human)] + trait Hero { + fn info_wide(&self, is_planet: bool) -> &str; + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero<__S>])] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface] + impl Character for Human { + fn id_wide(&self, is_number: bool) -> &str { + if is_number { + &self.id + } else { + "none" + } + } + } + + #[graphql_interface(dyn)] + impl Hero for Human { + fn info_wide(&self, is_planet: bool) -> &str { + if is_planet { + &self.home_planet + } else { + &self.id + } + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> CharacterValue { + Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + } + .into() + } + + fn hero(&self) -> Box> { + Box::new(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }) + } + } + + #[tokio::test] + async fn enum_resolves_id_field() { + let schema = schema(QueryRoot); + + for (input, expected) in &[ + ("{ character { idWide(isNumber: true) } }", "human-32"), + ("{ character { idWide(isNumber: false) } }", "none"), + ] { + let expected: &str = *expected; + + assert_eq!( + execute(*input, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"character": {"idWide": expected}}), vec![])), + ); + } + } + + #[tokio::test] + async fn dyn_resolves_info_field() { + let schema = schema(QueryRoot); + + for (input, expected) in &[ + ("{ hero { infoWide(isPlanet: true) } }", "earth"), + ("{ hero { infoWide(isPlanet: false) } }", "human-32"), + ] { + let expected: &str = *expected; + + assert_eq!( + execute(*input, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"hero": {"infoWide": expected}}), vec![])), + ); + } + } + + #[tokio::test] + async fn camelcases_name() { + let schema = schema(QueryRoot); + + for (interface, field, arg) in &[ + ("Character", "idWide", "isNumber"), + ("Hero", "infoWide", "isPlanet"), + ] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + fields {{ + name + args {{ + name + }} + }} + }} + }}"#, + interface, + ); + + let expected_field_name: &str = *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}]}, + ]}}), + vec![], + )), + ); + } + } + + #[tokio::test] + async fn has_no_description() { + let schema = schema(QueryRoot); + + for interface in &["Character", "Hero"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + fields {{ + args {{ + description + }} + }} + }} + }}"#, + interface, + ); + + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": { "fields": [{"args": [{"description": None}]}]}}), + vec![], + )), + ); + } + } + + #[tokio::test] + async fn has_no_defaults() { + let schema = schema(QueryRoot); + + for interface in &["Character", "Hero"] { + let doc = format!( + r#"{{ + __type(name: "{}") {{ + fields {{ + args {{ + defaultValue + }} + }} + }} + }}"#, + interface, + ); + + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": { "fields": [{"args": [{"defaultValue": None}]}]}}), + vec![], + )), + ); + } + } +} + +mod default_argument { + use super::*; + + #[graphql_interface(for = Human)] + trait Character { + async fn id( + &self, + #[graphql_interface(default)] first: String, + #[graphql_interface(default = "second".to_string())] second: String, + #[graphql_interface(default = "t")] third: String, + ) -> String; + } + + #[derive(GraphQLObject)] + #[graphql(impl = CharacterValue)] + struct Human { + id: String, + } + + #[graphql_interface] + impl Character for Human { + async fn id(&self, first: String, second: String, third: String) -> String { + format!("{}|{}&{}", first, second, third) + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> CharacterValue { + Human { + id: "human-32".to_string(), + } + .into() + } + } + + #[tokio::test] + async fn resolves_id_field() { + let schema = schema(QueryRoot); + + for (input, expected) in &[ + ("{ character { id } }", "|second&t"), + (r#"{ character { id(first: "first") } }"#, "first|second&t"), + (r#"{ character { id(second: "") } }"#, "|&t"), + ( + r#"{ character { id(first: "first", second: "") } }"#, + "first|&t", + ), + ( + r#"{ character { id(first: "first", second: "", third: "") } }"#, + "first|&", + ), + ] { + let expected: &str = *expected; + + assert_eq!( + execute(*input, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"character": {"id": expected}}), vec![])), + ); + } + } + + #[tokio::test] + async fn has_defaults() { + const DOC: &str = r#"{ + __type(name: "Character") { + fields { + args { + name + defaultValue + } + } + } + }"#; + + let schema = schema(QueryRoot); + + 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""#}, + ]}]}}), + vec![], + )), + ); + } +} + +mod description_from_doc_comment { + use super::*; + + /// Rust docs. + #[graphql_interface(for = Human)] + trait Character { + /// Rust `id` docs. + fn id(&self) -> &str; + } + + #[derive(GraphQLObject)] + #[graphql(impl = CharacterValue)] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface] + impl Character for Human { + fn id(&self) -> &str { + &self.id + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> CharacterValue { + Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + } + .into() + } + } + + #[tokio::test] + async fn uses_doc_comment_as_description() { + const DOC: &str = r#"{ + __type(name: "Character") { + 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 { + #![allow(deprecated)] + + use super::*; + + #[graphql_interface(for = Human)] + trait Character { + fn id(&self) -> &str; + + #[deprecated] + fn a(&self) -> &str { + "a" + } + + #[deprecated(note = "Use `id`.")] + fn b(&self) -> &str { + "b" + } + } + + #[derive(GraphQLObject)] + #[graphql(impl = CharacterValue)] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface] + impl Character for Human { + fn id(&self) -> &str { + &self.id + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> CharacterValue { + Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + } + .into() + } + } + + #[tokio::test] + async fn resolves_id_field() { + const DOC: &str = r#"{ + character { + id + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"character": {"id": "human-32"}}), vec![])), + ); + } + + #[tokio::test] + async fn resolves_deprecated_fields() { + const DOC: &str = r#"{ + character { + a + b + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"character": {"a": "a", "b": "b"}}), vec![],)), + ); + } + + #[tokio::test] + async fn deprecates_fields() { + const DOC: &str = r#"{ + __type(name: "Character") { + 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: "Character") { + 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 { + #![allow(deprecated)] + + use super::*; + + /// Rust docs. + #[graphql_interface(name = "MyChar", desc = "My character.", for = Human)] + trait Character { + /// Rust `id` docs. + #[graphql_interface(name = "myId", desc = "My character ID.", deprecated = "Not used.")] + #[deprecated(note = "Should be omitted.")] + fn id( + &self, + #[graphql_interface(name = "myName", desc = "My argument.", default)] n: Option, + ) -> &str; + + #[graphql_interface(deprecated)] + #[deprecated(note = "Should be omitted.")] + fn a(&self) -> &str { + "a" + } + + fn b(&self) -> &str { + "b" + } + } + + #[derive(GraphQLObject)] + #[graphql(impl = CharacterValue)] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface] + impl Character for Human { + fn id(&self, _: Option) -> &str { + &self.id + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> CharacterValue { + Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + } + .into() + } + } + + #[tokio::test] + async fn resolves_fields() { + const DOC: &str = r#"{ + character { + myId + a + b + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"myId": "human-32", "a": "a", "b": "b"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_custom_name() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + 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": "MyChar", + "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: "MyChar") { + 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 character.", + "fields": [{ + "name": "myId", + "description": "My character 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: "MyChar") { + 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 explicit_scalar { + use super::*; + + #[graphql_interface(for = [Human, Droid])] + #[graphql_interface(scalar = DefaultScalarValue)] + trait Character { + fn id(&self) -> &str; + } + + #[graphql_interface(dyn = DynHero, for = [Human, Droid])] + #[graphql_interface(scalar = DefaultScalarValue)] + trait Hero { + async fn info(&self) -> &str; + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero], scalar = DefaultScalarValue)] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface(scalar = DefaultScalarValue)] + impl Character for Human { + fn id(&self) -> &str { + &self.id + } + } + + #[graphql_interface(dyn, scalar = DefaultScalarValue)] + impl Hero for Human { + async fn info(&self) -> &str { + &self.home_planet + } + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero], scalar = DefaultScalarValue)] + struct Droid { + id: String, + primary_function: String, + } + + #[graphql_interface(scalar = DefaultScalarValue)] + impl Character for Droid { + fn id(&self) -> &str { + &self.id + } + } + + #[graphql_interface(dyn, scalar = DefaultScalarValue)] + impl Hero for Droid { + async fn info(&self) -> &str { + &self.primary_function + } + } + + #[derive(Clone, Copy)] + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(scalar = DefaultScalarValue)] + 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(), + 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::<_, DefaultScalarValue, _>(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::<_, DefaultScalarValue, _>(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::<_, DefaultScalarValue, _>(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::<_, DefaultScalarValue, _>(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::<_, DefaultScalarValue, _>(*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::<_, DefaultScalarValue, _>(*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 custom_scalar { + use crate::custom_scalar::MyScalarValue; + + use super::*; + + #[graphql_interface(for = [Human, Droid], scalar = MyScalarValue)] + trait Character { + async fn id(&self) -> &str; + } + + #[graphql_interface(dyn = DynHero, for = [Human, Droid])] + #[graphql_interface(scalar = MyScalarValue)] + trait Hero { + async fn info(&self) -> &str; + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero], scalar = MyScalarValue)] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface(scalar = MyScalarValue)] + impl Character for Human { + async fn id(&self) -> &str { + &self.id + } + } + + #[graphql_interface(dyn, scalar = MyScalarValue)] + impl Hero for Human { + async fn info(&self) -> &str { + &self.home_planet + } + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero], scalar = MyScalarValue)] + struct Droid { + id: String, + primary_function: String, + } + + #[graphql_interface(scalar = MyScalarValue)] + impl Character for Droid { + async fn id(&self) -> &str { + &self.id + } + } + + #[graphql_interface(dyn, scalar = MyScalarValue)] + impl Hero for Droid { + async fn info(&self) -> &str { + &self.primary_function + } + } + + #[derive(Clone, Copy)] + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(scalar = MyScalarValue)] + 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(), + 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::<_, MyScalarValue, _>(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::<_, MyScalarValue, _>(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::<_, MyScalarValue, _>(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::<_, MyScalarValue, _>(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::<_, MyScalarValue, _>(*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::<_, MyScalarValue, _>(*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 explicit_generic_scalar { + use super::*; + + #[graphql_interface(for = [Human, Droid], scalar = S)] + trait Character { + fn id(&self) -> FieldResult<&str, S>; + } + + #[graphql_interface(dyn = DynHero, for = [Human, Droid], scalar = S)] + trait Hero { + async fn info(&self) -> FieldResult<&str, S>; + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue<__S>, DynHero<__S>])] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface(scalar = S)] + impl Character for Human { + fn id(&self) -> FieldResult<&str, S> { + Ok(&self.id) + } + } + + #[graphql_interface(dyn, scalar = S)] + impl Hero for Human { + async fn info(&self) -> FieldResult<&str, S> { + Ok(&self.home_planet) + } + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue<__S>, DynHero<__S>])] + struct Droid { + id: String, + primary_function: String, + } + + #[graphql_interface(scalar = S)] + impl Character for Droid { + fn id(&self) -> FieldResult<&str, S> { + Ok(&self.id) + } + } + + #[graphql_interface(dyn, scalar = S)] + impl Hero for Droid { + async fn info(&self) -> FieldResult<&str, S> { + Ok(&self.primary_function) + } + } + + #[derive(Clone, Copy)] + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(scalar = DefaultScalarValue)] + 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(), + 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 explicit_custom_context { + use super::*; + + pub struct CustomContext; + + impl juniper::Context for CustomContext {} + + #[graphql_interface(for = [Human, Droid], context = CustomContext)] + trait Character { + async fn id<'a>(&'a self, context: &CustomContext) -> &'a str; + + async fn info<'b>(&'b self, ctx: &()) -> &'b str; + + fn more<'c>(&'c self, #[graphql_interface(context)] custom: &CustomContext) -> &'c str; + } + + #[graphql_interface(dyn = DynHero, for = [Human, Droid])] + #[graphql_interface(context = CustomContext)] + trait Hero { + async fn id<'a>(&'a self, context: &CustomContext) -> &'a str; + + async fn info<'b>(&'b self, ctx: &()) -> &'b str; + + fn more<'c>(&'c self, #[graphql_interface(context)] custom: &CustomContext) -> &'c str; + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero<__S>], context = CustomContext)] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface] + impl Character for Human { + async fn id<'a>(&'a self, _: &CustomContext) -> &'a str { + &self.id + } + + async fn info<'b>(&'b self, _: &()) -> &'b str { + &self.home_planet + } + + fn more(&self, _: &CustomContext) -> &'static str { + "human" + } + } + + #[graphql_interface(dyn)] + impl Hero for Human { + async fn id<'a>(&'a self, _: &CustomContext) -> &'a str { + &self.id + } + + async fn info<'b>(&'b self, _: &()) -> &'b str { + &self.home_planet + } + + fn more(&self, _: &CustomContext) -> &'static str { + "human" + } + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero<__S>], context = CustomContext)] + struct Droid { + id: String, + primary_function: String, + } + + #[graphql_interface] + impl Character for Droid { + async fn id<'a>(&'a self, _: &CustomContext) -> &'a str { + &self.id + } + + async fn info<'b>(&'b self, _: &()) -> &'b str { + &self.primary_function + } + + fn more(&self, _: &CustomContext) -> &'static str { + "droid" + } + } + + #[graphql_interface(dyn)] + impl Hero for Droid { + async fn id<'a>(&'a self, _: &CustomContext) -> &'a str { + &self.id + } + + async fn info<'b>(&'b self, _: &()) -> &'b str { + &self.primary_function + } + + fn more(&self, _: &CustomContext) -> &'static str { + "droid" + } + } + + #[derive(Clone, Copy)] + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(context = CustomContext)] + 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(), + 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(), &CustomContext).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(), &CustomContext).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(), &CustomContext).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(), &CustomContext).await, + Ok(( + graphql_value!({"hero": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_fields() { + for interface in &["character", "hero"] { + let doc = format!( + r#"{{ + {} {{ + id + info + more + }} + }}"#, + interface, + ); + + let expected_interface: &str = *interface; + + for (root, expected_id, expected_info, expexted_more) in &[ + (QueryRoot::Human, "human-32", "earth", "human"), + (QueryRoot::Droid, "droid-99", "run", "droid"), + ] { + let schema = schema(*root); + + let expected_id: &str = *expected_id; + let expected_info: &str = *expected_info; + let expexted_more: &str = *expexted_more; + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &CustomContext).await, + Ok(( + graphql_value!({expected_interface: { + "id": expected_id, + "info": expected_info, + "more": expexted_more, + }}), + vec![], + )), + ); + } + } + } +} + +mod inferred_custom_context_from_field { + use super::*; + + pub struct CustomContext(String); + + impl juniper::Context for CustomContext {} + + #[graphql_interface(for = [Human, Droid])] + trait Character { + fn id<'a>(&self, context: &'a CustomContext) -> &'a str; + + fn info<'b>(&'b self, context: &()) -> &'b str; + } + + #[graphql_interface(dyn = DynHero, for = [Human, Droid])] + trait Hero { + async fn id<'a>(&self, context: &'a CustomContext) -> &'a str; + + async fn info<'b>(&'b self, context: &()) -> &'b str; + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero<__S>], context = CustomContext)] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface] + impl Character for Human { + fn id<'a>(&self, ctx: &'a CustomContext) -> &'a str { + &ctx.0 + } + + fn info<'b>(&'b self, _: &()) -> &'b str { + &self.home_planet + } + } + + #[graphql_interface(dyn)] + impl Hero for Human { + async fn id<'a>(&self, ctx: &'a CustomContext) -> &'a str { + &ctx.0 + } + + async fn info<'b>(&'b self, _: &()) -> &'b str { + &self.home_planet + } + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero<__S>], context = CustomContext)] + struct Droid { + id: String, + primary_function: String, + } + + #[graphql_interface] + impl Character for Droid { + fn id<'a>(&self, ctx: &'a CustomContext) -> &'a str { + &ctx.0 + } + + fn info<'b>(&'b self, _: &()) -> &'b str { + &self.primary_function + } + } + + #[graphql_interface(dyn)] + impl Hero for Droid { + async fn id<'a>(&self, ctx: &'a CustomContext) -> &'a str { + &ctx.0 + } + + async fn info<'b>(&'b self, _: &()) -> &'b str { + &self.primary_function + } + } + + #[derive(Clone, Copy)] + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(context = CustomContext)] + 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(), + 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); + let ctx = CustomContext("in-ctx".into()); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &ctx).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); + let ctx = CustomContext("in-droid".into()); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &ctx).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); + let ctx = CustomContext("in-ctx".into()); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &ctx).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); + let ctx = CustomContext("in-droid".into()); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &ctx).await, + Ok(( + graphql_value!({"hero": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_fields() { + for interface in &["character", "hero"] { + let doc = format!( + r#"{{ + {} {{ + id + info + }} + }}"#, + interface, + ); + + let expected_interface: &str = *interface; + + for (root, expected_id, expected_info) in &[ + (QueryRoot::Human, "human-ctx", "earth"), + (QueryRoot::Droid, "droid-ctx", "run"), + ] { + let schema = schema(*root); + let ctx = CustomContext(expected_id.to_string()); + + let expected_id: &str = *expected_id; + let expected_info: &str = *expected_info; + assert_eq!( + execute(&doc, None, &schema, &Variables::new(), &ctx).await, + Ok(( + graphql_value!({expected_interface: { + "id": expected_id, + "info": expected_info, + }}), + vec![], + )), + ); + } + } + } +} + +mod inferred_custom_context_from_downcast { + use super::*; + + struct Database { + droid: Option, + } + + impl juniper::Context for Database {} + + #[graphql_interface(for = [Human, Droid])] + trait Character { + #[graphql_interface(downcast)] + fn as_human<'s>(&'s self, _: &Database) -> Option<&'s Human>; + + async fn id(&self) -> &str; + } + + #[graphql_interface(dyn = DynHero, for = [Human, Droid])] + trait Hero { + #[graphql_interface(downcast)] + fn as_droid<'db>(&self, db: &'db Database) -> Option<&'db Droid>; + + async fn info(&self) -> &str; + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero<__S>], context = Database)] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface] + impl Character for Human { + fn as_human<'s>(&'s self, _: &Database) -> Option<&'s Human> { + Some(self) + } + + async fn id(&self) -> &str { + &self.id + } + } + + #[graphql_interface(dyn)] + impl Hero for Human { + fn as_droid<'db>(&self, _: &'db Database) -> Option<&'db Droid> { + None + } + + async fn info(&self) -> &str { + &self.home_planet + } + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero<__S>], context = Database)] + struct Droid { + id: String, + primary_function: String, + } + + #[graphql_interface] + impl Character for Droid { + fn as_human<'s>(&'s self, _: &Database) -> Option<&'s Human> { + None + } + + async fn id(&self) -> &str { + &self.id + } + } + + #[graphql_interface(dyn)] + impl Hero for Droid { + fn as_droid<'db>(&self, db: &'db Database) -> Option<&'db Droid> { + db.droid.as_ref() + } + + async fn info(&self) -> &str { + &self.primary_function + } + } + + #[derive(Clone)] + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(context = Database)] + 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(), + 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); + let db = Database { droid: None }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).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); + let db = Database { + droid: Some(Droid { + id: "droid-88".to_string(), + primary_function: "sit".to_string(), + }), + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).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); + let db = Database { droid: None }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).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); + let db = Database { + droid: Some(Droid { + id: "droid-88".to_string(), + primary_function: "sit".to_string(), + }), + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"hero": {"droidId": "droid-88", "primaryFunction": "sit"}}), + 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.clone()); + let db = Database { droid: None }; + + let expected_id: &str = *expected_id; + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).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.clone()); + let db = Database { droid: None }; + + let expected_info: &str = *expected_info; + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok((graphql_value!({"hero": {"info": expected_info}}), vec![])), + ); + } + } +} + +mod executor { + use juniper::LookAheadMethods as _; + + use super::*; + + #[graphql_interface(for = [Human, Droid], scalar = S)] + trait Character { + async fn id<'a>(&self, executor: &'a Executor<'_, '_, (), S>) -> &'a str + where + S: Send + Sync, + { + executor.look_ahead().field_name() + } + + async fn info<'b>( + &'b self, + #[graphql_interface(executor)] another: &Executor<'_, '_, (), S>, + ) -> &'b str + where + S: Send + Sync; + } + + #[graphql_interface(dyn = DynHero, for = [Human, Droid], scalar = S)] + trait Hero { + async fn id<'a>(&self, executor: &'a Executor<'_, '_, (), S>) -> &'a str + where + S: Send + Sync, + { + executor.look_ahead().field_name() + } + + async fn info<'b>( + &'b self, + #[graphql_interface(executor)] another: &Executor<'_, '_, (), S>, + ) -> &'b str + where + S: Send + Sync; + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue<__S>, DynHero<__S>])] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface(scalar = S)] + impl Character for Human { + async fn info<'b>(&'b self, _: &Executor<'_, '_, (), S>) -> &'b str + where + S: Send + Sync, + { + &self.home_planet + } + } + + #[graphql_interface(dyn, scalar = S)] + impl Hero for Human { + async fn info<'b>(&'b self, _: &Executor<'_, '_, (), S>) -> &'b str + where + S: Send + Sync, + { + &self.home_planet + } + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue<__S>, DynHero<__S>])] + struct Droid { + id: String, + primary_function: String, + } + + #[graphql_interface(scalar = S)] + impl Character for Droid { + async fn info<'b>(&'b self, _: &Executor<'_, '_, (), S>) -> &'b str + where + S: Send + Sync, + { + &self.primary_function + } + } + + #[graphql_interface(dyn, scalar = S)] + impl Hero for Droid { + async fn info<'b>(&'b self, _: &Executor<'_, '_, (), S>) -> &'b str + where + S: Send + Sync, + { + &self.primary_function + } + } + + #[derive(Clone, Copy)] + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(scalar = DefaultScalarValue)] + 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(), + 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 resolves_fields() { + for interface in &["character", "hero"] { + let doc = format!( + r#"{{ + {} {{ + id + info + }} + }}"#, + interface, + ); + + let expected_interface: &str = *interface; + + 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!({expected_interface: {"id": "id", "info": expected_info}}), + vec![], + )), + ); + } + } + } +} + +mod ignored_method { + use super::*; + + #[graphql_interface(for = Human)] + trait Character { + fn id(&self) -> &str; + + #[graphql_interface(ignore)] + fn ignored(&self) -> Option<&Human> { + None + } + + #[graphql_interface(skip)] + fn skipped(&self) {} + } + + #[derive(GraphQLObject)] + #[graphql(impl = CharacterValue)] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface] + impl Character for Human { + fn id(&self) -> &str { + &self.id + } + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> CharacterValue { + Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + } + .into() + } + } + + #[tokio::test] + async fn resolves_human() { + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_id_field() { + const DOC: &str = r#"{ + character { + id + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"character": {"id": "human-32"}}), vec![])), + ); + } + + #[tokio::test] + async fn is_not_field() { + const DOC: &str = r#"{ + __type(name: "Character") { + fields { + name + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [{"name": "id"}]}}), + vec![], + )), + ); + } +} + +mod downcast_method { + use super::*; + + #[graphql_interface(for = [Human, Droid])] + trait Character { + fn id(&self) -> &str; + + #[graphql_interface(downcast)] + fn as_human(&self) -> Option<&Human> { + None + } + } + + #[graphql_interface(dyn = DynHero, for = [Human, Droid])] + trait Hero { + fn info(&self) -> &str; + + #[graphql_interface(downcast)] + fn as_droid(&self) -> Option<&Droid> { + None + } + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero<__S>])] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface] + impl Character for Human { + fn id(&self) -> &str { + &self.id + } + + fn as_human(&self) -> Option<&Human> { + Some(self) + } + } + + #[graphql_interface(dyn)] + impl Hero for Human { + fn info(&self) -> &str { + &self.home_planet + } + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero<__S>])] + struct Droid { + id: String, + primary_function: String, + } + + #[graphql_interface] + impl Character for Droid { + fn id(&self) -> &str { + &self.id + } + } + + #[graphql_interface(dyn)] + impl Hero for Droid { + fn info(&self) -> &str { + &self.primary_function + } + + fn as_droid(&self) -> Option<&Droid> { + Some(self) + } + } + + #[derive(Clone)] + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + 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(), + 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.clone()); + + 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.clone()); + + let expected_info: &str = *expected_info; + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"hero": {"info": expected_info}}), vec![])), + ); + } + } + + #[tokio::test] + async fn is_not_field() { + let schema = schema(QueryRoot::Human); + + for (doc, field) in &[ + (r#"{__type(name: "Character") { fields { name } } }"#, "id"), + (r#"{__type(name: "Hero") { fields { name } } }"#, "info"), + ] { + let expected_field: &str = *field; + + assert_eq!( + execute(*doc, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"fields": [{"name": expected_field}]}}), + vec![], + )), + ); + } + } +} + +mod external_downcast { + use super::*; + + struct Database { + human: Option, + droid: Option, + } + + impl juniper::Context for Database {} + + #[graphql_interface(for = [Human, Droid])] + #[graphql_interface(context = Database, on Human = CharacterValue::as_human)] + trait Character { + fn id(&self) -> &str; + } + + impl CharacterValue { + fn as_human<'db>(&self, db: &'db Database) -> Option<&'db Human> { + db.human.as_ref() + } + } + + #[graphql_interface(dyn = DynHero, for = [Human, Droid])] + #[graphql_interface(context = Database)] + #[graphql_interface(on Droid = DynHero::as_droid)] + trait Hero { + fn info(&self) -> &str; + } + + impl<'a, S: ScalarValue> DynHero<'a, S> { + fn as_droid<'db>(&self, db: &'db Database) -> Option<&'db Droid> { + db.droid.as_ref() + } + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero<__S>], context = Database)] + struct Human { + id: String, + home_planet: String, + } + + #[graphql_interface] + impl Character for Human { + fn id(&self) -> &str { + &self.id + } + } + + #[graphql_interface(dyn)] + impl Hero for Human { + fn info(&self) -> &str { + &self.home_planet + } + } + + #[derive(GraphQLObject)] + #[graphql(impl = [CharacterValue, DynHero<__S>], context = Database)] + struct Droid { + id: String, + primary_function: String, + } + + #[graphql_interface] + impl Character for Droid { + fn id(&self) -> &str { + &self.id + } + } + + #[graphql_interface(dyn)] + impl Hero for Droid { + fn info(&self) -> &str { + &self.primary_function + } + } + + #[derive(Clone)] + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(context = Database)] + 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(), + 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); + let db = Database { + human: Some(Human { + id: "human-64".to_string(), + home_planet: "mars".to_string(), + }), + droid: None, + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"character": {"humanId": "human-64", "homePlanet": "mars"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn enum_resolves_droid() { + const DOC: &str = r#"{ + character { + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + let schema = schema(QueryRoot::Droid); + let db = Database { + human: None, + droid: None, + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).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); + let db = Database { + human: None, + droid: None, + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).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); + let db = Database { + human: None, + droid: Some(Droid { + id: "droid-01".to_string(), + primary_function: "swim".to_string(), + }), + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"hero": {"droidId": "droid-01", "primaryFunction": "swim"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn enum_resolves_id_field() { + const DOC: &str = r#"{ + character { + id + } + }"#; + + let db = Database { + human: Some(Human { + id: "human-64".to_string(), + home_planet: "mars".to_string(), + }), + droid: None, + }; + + for (root, expected_id) in &[ + (QueryRoot::Human, "human-32"), + (QueryRoot::Droid, "droid-99"), + ] { + let schema = schema(root.clone()); + + let expected_id: &str = *expected_id; + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok((graphql_value!({"character": {"id": expected_id}}), vec![])), + ); + } + } + + #[tokio::test] + async fn dyn_resolves_info_field() { + const DOC: &str = r#"{ + hero { + info + } + }"#; + + let db = Database { + human: None, + droid: Some(Droid { + id: "droid-01".to_string(), + primary_function: "swim".to_string(), + }), + }; + + for (root, expected_info) in &[(QueryRoot::Human, "earth"), (QueryRoot::Droid, "run")] { + let schema = schema(root.clone()); + + let expected_info: &str = *expected_info; + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok((graphql_value!({"hero": {"info": expected_info}}), vec![])), + ); + } + } +} diff --git a/integration_tests/juniper_tests/src/codegen/mod.rs b/integration_tests/juniper_tests/src/codegen/mod.rs index 99d2b365..37e12c31 100644 --- a/integration_tests/juniper_tests/src/codegen/mod.rs +++ b/integration_tests/juniper_tests/src/codegen/mod.rs @@ -4,6 +4,7 @@ mod derive_object; mod derive_object_with_raw_idents; mod impl_object; mod impl_scalar; +mod interface_attr; mod scalar_value_transparent; mod union_attr; mod union_derive; diff --git a/integration_tests/juniper_tests/src/codegen/union_attr.rs b/integration_tests/juniper_tests/src/codegen/union_attr.rs index 2581512c..ebad8221 100644 --- a/integration_tests/juniper_tests/src/codegen/union_attr.rs +++ b/integration_tests/juniper_tests/src/codegen/union_attr.rs @@ -305,7 +305,7 @@ mod generic { } } -mod description_from_doc_comments { +mod description_from_doc_comment { use super::*; /// Rust docs. @@ -647,93 +647,6 @@ mod custom_scalar { } } -mod inferred_custom_context { - use super::*; - - #[graphql_union] - trait Character { - fn as_human(&self, _: &CustomContext) -> Option<&HumanCustomContext> { - None - } - fn as_droid(&self, _: &()) -> Option<&DroidCustomContext> { - None - } - } - - impl Character for HumanCustomContext { - fn as_human(&self, _: &CustomContext) -> Option<&HumanCustomContext> { - Some(&self) - } - } - - impl Character for DroidCustomContext { - fn as_droid(&self, _: &()) -> Option<&DroidCustomContext> { - Some(&self) - } - } - - type DynCharacter<'a> = dyn Character + Send + Sync + 'a; - - struct QueryRoot; - - #[graphql_object(context = CustomContext)] - impl QueryRoot { - fn character(&self, ctx: &CustomContext) -> Box> { - let ch: Box> = match ctx { - CustomContext::Human => Box::new(HumanCustomContext { - id: "human-32".to_string(), - home_planet: "earth".to_string(), - }), - CustomContext::Droid => Box::new(DroidCustomContext { - id: "droid-99".to_string(), - primary_function: "run".to_string(), - }), - _ => unimplemented!(), - }; - ch - } - } - - const DOC: &str = r#"{ - character { - ... on HumanCustomContext { - humanId: id - homePlanet - } - ... on DroidCustomContext { - droidId: id - primaryFunction - } - } - }"#; - - #[tokio::test] - async fn resolves_human() { - let schema = schema(QueryRoot); - - assert_eq!( - execute(DOC, None, &schema, &Variables::new(), &CustomContext::Human).await, - Ok(( - graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), - vec![], - )), - ); - } - - #[tokio::test] - async fn resolves_droid() { - let schema = schema(QueryRoot); - - assert_eq!( - execute(DOC, None, &schema, &Variables::new(), &CustomContext::Droid).await, - Ok(( - graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), - vec![], - )), - ); - } -} - mod explicit_custom_context { use super::*; @@ -821,7 +734,94 @@ mod explicit_custom_context { } } -mod ignored_methods { +mod inferred_custom_context { + use super::*; + + #[graphql_union] + trait Character { + fn as_human(&self, _: &CustomContext) -> Option<&HumanCustomContext> { + None + } + fn as_droid(&self, _: &()) -> Option<&DroidCustomContext> { + None + } + } + + impl Character for HumanCustomContext { + fn as_human(&self, _: &CustomContext) -> Option<&HumanCustomContext> { + Some(&self) + } + } + + impl Character for DroidCustomContext { + fn as_droid(&self, _: &()) -> Option<&DroidCustomContext> { + Some(&self) + } + } + + type DynCharacter<'a> = dyn Character + Send + Sync + 'a; + + struct QueryRoot; + + #[graphql_object(context = CustomContext)] + impl QueryRoot { + fn character(&self, ctx: &CustomContext) -> Box> { + let ch: Box> = match ctx { + CustomContext::Human => Box::new(HumanCustomContext { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + CustomContext::Droid => Box::new(DroidCustomContext { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + _ => unimplemented!(), + }; + ch + } + } + + const DOC: &str = r#"{ + character { + ... on HumanCustomContext { + humanId: id + homePlanet + } + ... on DroidCustomContext { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Human).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Droid).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } +} + +mod ignored_method { use super::*; #[graphql_union] diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index 882abe6e..332454a6 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -18,43 +18,44 @@ edition = "2018" [badges] travis-ci = { repository = "graphql-rust/juniper" } -[[bench]] -name = "bench" -harness = false -path = "benches/bench.rs" - [features] -expose-test-schema = ["anyhow", "serde_json"] -schema-language = ["graphql-parser-integration"] -graphql-parser-integration = ["graphql-parser"] default = [ "bson", "chrono", + "schema-language", "url", "uuid", - "schema-language", ] +expose-test-schema = ["anyhow", "serde_json"] +graphql-parser-integration = ["graphql-parser"] scalar-naivetime = [] +schema-language = ["graphql-parser-integration"] [dependencies] juniper_codegen = { version = "0.14.2", path = "../juniper_codegen" } anyhow = { default-features = false, version = "1.0.32", optional = true } +async-trait = "0.1.39" bson = { version = "1.0", optional = true } chrono = { default-features = false, version = "0.4", optional = true } fnv = "1.0.3" futures = { default-features = false, features = ["alloc"], version = "0.3.1" } futures-enum = "0.1.12" +graphql-parser = { version = "0.3", optional = true } indexmap = { version = "1.0", features = ["serde-1"] } -serde = { default-features = false, version = "1.0.8", features = ["derive"] } -serde_json = { default-features = false, version = "1.0", optional = true } +serde = { version = "1.0.8", features = ["derive"], default-features = false } +serde_json = { version = "1.0.2", default-features = false, optional = true } static_assertions = "1.1" url = { version = "2.0", optional = true } -uuid = { default-features = false, version = "0.8", optional = true } -graphql-parser = { version = "0.3", optional = true } +uuid = { version = "0.8", default-features = false, optional = true } [dev-dependencies] bencher = "0.1.2" +pretty_assertions = "0.6.1" serde_json = { version = "1.0.2" } tokio = { version = "0.2", features = ["macros", "rt-core", "time"] } -pretty_assertions = "0.6.1" + +[[bench]] +name = "bench" +harness = false +path = "benches/bench.rs" diff --git a/juniper/src/executor/mod.rs b/juniper/src/executor/mod.rs index 07be7ac9..92a9648f 100644 --- a/juniper/src/executor/mod.rs +++ b/juniper/src/executor/mod.rs @@ -1025,10 +1025,9 @@ where let mut fragments = vec![]; for def in document.iter() { - match def { - Definition::Fragment(f) => fragments.push(f), - _ => (), - }; + if let Definition::Fragment(f) = def { + fragments.push(f) + } } let default_variable_values = operation.item.variable_definitions.as_ref().map(|defs| { diff --git a/juniper/src/executor_tests/introspection/mod.rs b/juniper/src/executor_tests/introspection/mod.rs index aacb341f..eeb4e144 100644 --- a/juniper/src/executor_tests/introspection/mod.rs +++ b/juniper/src/executor_tests/introspection/mod.rs @@ -7,6 +7,7 @@ use self::input_object::{NamedPublic, NamedPublicWithDescription}; use crate::{ executor::Variables, + graphql_interface, graphql_object, schema::model::RootNode, types::scalars::{EmptyMutation, EmptySubscription}, value::{DefaultScalarValue, ParseScalarResult, ParseScalarValue, Value}, @@ -22,10 +23,6 @@ enum Sample { struct Scalar(i32); -struct Interface; - -struct Root; - #[crate::graphql_scalar(name = "SampleScalar")] impl GraphQLScalar for Scalar { fn resolve(&self) -> Value { @@ -41,23 +38,19 @@ impl GraphQLScalar for Scalar { } } -graphql_interface!(Interface: () as "SampleInterface" |&self| { - description: "A sample interface" - - field sample_enum() -> Sample as "A sample field in the interface" { +/// A sample interface +#[graphql_interface(name = "SampleInterface", for = Root, scalar = DefaultScalarValue)] +trait Interface { + /// A sample field in the interface + fn sample_enum(&self) -> Sample { Sample::One } +} - instance_resolvers: |&_| { - Root => Some(Root), - } -}); +struct Root; /// The root query object in the schema -#[crate::graphql_object( - interfaces = [&Interface] - Scalar = crate::DefaultScalarValue, -)] +#[graphql_object(interfaces = InterfaceValue)] impl Root { fn sample_enum() -> Sample { Sample::One @@ -65,7 +58,7 @@ impl Root { #[graphql(arguments( first(description = "The first number",), - second(description = "The second number", default = 123,), + second(description = "The second number", default = 123), ))] /// A sample scalar field on the object @@ -74,6 +67,9 @@ impl Root { } } +#[graphql_interface(scalar = DefaultScalarValue)] +impl Interface for Root {} + #[tokio::test] async fn test_execution() { let doc = r#" diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index 154ab67c..5ac999b7 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -119,7 +119,7 @@ extern crate bson; // These are required by the code generated via the `juniper_codegen` macros. #[doc(hidden)] -pub use {futures, static_assertions as sa}; +pub use {async_trait::async_trait, futures, static_assertions as sa}; #[doc(inline)] pub use futures::future::{BoxFuture, LocalBoxFuture}; @@ -128,8 +128,8 @@ pub use futures::future::{BoxFuture, LocalBoxFuture}; // This allows users to just depend on juniper and get the derive // functionality automatically. pub use juniper_codegen::{ - graphql_object, graphql_scalar, graphql_subscription, graphql_union, GraphQLEnum, - GraphQLInputObject, GraphQLObject, GraphQLScalarValue, GraphQLUnion, + graphql_interface, graphql_object, graphql_scalar, graphql_subscription, graphql_union, + GraphQLEnum, GraphQLInputObject, GraphQLObject, GraphQLScalarValue, GraphQLUnion, }; #[macro_use] @@ -175,15 +175,18 @@ pub use crate::{ LookAheadSelection, LookAheadValue, OwnedExecutor, Registry, ValuesStream, Variables, }, introspection::IntrospectionFormat, - macros::subscription_helpers::{ExtractTypeFromStream, IntoFieldResult}, + macros::helper::{ + subscription::{ExtractTypeFromStream, IntoFieldResult}, + AsDynGraphQLValue, + }, schema::{ meta, model::{RootNode, SchemaType}, }, types::{ - async_await::{GraphQLTypeAsync, GraphQLValueAsync}, - base::{Arguments, GraphQLType, GraphQLValue, TypeKind}, - marker::{self, GraphQLUnion, IsOutputType}, + async_await::{DynGraphQLValueAsync, GraphQLTypeAsync, GraphQLValueAsync}, + base::{Arguments, DynGraphQLValue, GraphQLType, GraphQLValue, TypeKind}, + marker::{self, GraphQLInterface, GraphQLUnion}, scalars::{EmptyMutation, EmptySubscription, ID}, subscriptions::{ ExecutionOutput, GraphQLSubscriptionType, GraphQLSubscriptionValue, diff --git a/juniper/src/macros/common.rs b/juniper/src/macros/common.rs deleted file mode 100644 index 9178f5a7..00000000 --- a/juniper/src/macros/common.rs +++ /dev/null @@ -1,741 +0,0 @@ -#[doc(hidden)] -#[macro_export] -macro_rules! __juniper_impl_trait { - ( - impl< < DefaultScalarValue > $(, $other: tt)* > $impl_trait:tt for $name:ty { - $($body:tt)+ - } - ) => { - impl<$($other,)*> $crate::$impl_trait<$crate::DefaultScalarValue> for $name { - $($body)* - } - }; - ( - impl< < DefaultScalarValue > $(, $other: tt)* > $impl_trait:tt for $name:ty - where ( $($where:tt)* ) - { - $($body:tt)+ - } - ) => { - impl<$($other,)*> $crate::$impl_trait<$crate::DefaultScalarValue> for $name - where $($where)* - { - $($body)* - } - }; - - ( - impl< <$generic:tt $(: $bound: tt)*> $(, $other: tt)* > $impl_trait:tt for $name:ty { - $($body:tt)* - } - ) => { - impl<$($other,)* $generic $(: $bound)*> $crate::$impl_trait<$generic> for $name - where - $generic: $crate::ScalarValue, - { - $($body)* - } - }; - ( - impl< <$generic:tt $(: $bound: tt)*> $(, $other: tt)* > $impl_trait:tt for $name:ty - where ( $($where:tt)* ) - { - $($body:tt)* - } - ) => { - impl<$($other,)* $generic $(: $bound)*> $crate::$impl_trait<$generic> for $name - where - $($where)* - $generic: $crate::ScalarValue, - { - $($body)* - } - }; - - ( - impl<$scalar:ty $(, $other: tt )*> $impl_trait:tt for $name:ty { - $($body:tt)* - } - ) => { - impl<$($other, )*> $crate::$impl_trait<$scalar> for $name { - $($body)* - } - }; - ( - impl<$scalar:ty $(, $other: tt )*> $impl_trait:tt for $name:ty - where ( $($where:tt)* ) - { - $($body:tt)* - } - ) => { - impl<$($other, )*> $crate::$impl_trait<$scalar> for $name - where $($where)* - { - $($body)* - } - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __juniper_insert_generic { - () => { - $crate::DefaultScalarValue - }; - ( - <$generic:tt $(: $bound: tt)*> - ) => { - $generic - }; - ( - $scalar: ty - ) => { - $scalar - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __juniper_parse_object_header { - ( - callback = $callback:ident, - rest = <$($lifetime:tt),*> $name: ty $(: $ctxt: ty)* as $outname: tt - where Scalar = <$generic:tt $(: $bound:tt)*> $(| &$mainself:ident |)* { - $($items: tt)* - } - ) => { - $crate::$callback!( - @parse, - meta = { - lifetimes = [$($lifetime,)*], - name = $name, - $(ctx = $ctxt,)* - $(main_self = $mainself,)* - outname = {$outname}, - scalar = {<$generic $(: $bound)*>}, - }, - rest = $($items)* - ); - }; - - ( - callback = $callback:ident, - rest = <$($lifetime:tt),*> $name: ty $(: $ctxt: ty)* as $outname: tt - where Scalar = $scalar: ty $(| &$mainself:ident |)* { - $($items: tt)* - } - ) => { - $crate::$callback!( - @parse, - meta = { - lifetimes = [$($lifetime,)*], - name = $name, - $(ctx = $ctxt,)* - $(main_self = $mainself,)* - outname = {$outname}, - scalar = {$scalar}, - }, - rest = $($items)* - ); - }; - - ( - callback = $callback: ident, - rest = <$($lifetime:tt),*> $name: ty $(: $ctxt: ty)* as $outname: tt $(| &$mainself:ident |)* { - $($items: tt)* - } - ) => { - $crate::$callback!( - @parse, - meta = { - lifetimes = [$($lifetime,)*], - name = $name, - $(ctx = $ctxt,)* - $(main_self = $mainself,)* - outname = {$outname}, - scalar = {}, - }, - rest = $($items)* - ); - }; - - ( - callback = $callback: ident, - rest = $name: ty $(: $ctxt: ty)* as $outname: tt - where Scalar = <$generic:tt $(: $bound:tt)*> $(| &$mainself:ident |)* { - $($items: tt)* - } - ) => { - $crate::$callback!( - @parse, - meta = { - lifetimes = [], - name = $name, - $(ctx = $ctxt,)* - $(main_self = $mainself,)* - outname = {$outname}, - scalar = {<$generic $(:$bound)*>}, - }, - rest = $($items)* - ); - }; - - ( - callback = $callback: ident, - rest = $name: ty $(: $ctxt: ty)* as $outname: tt - where Scalar = $scalar: ty $(| &$mainself:ident |)* { - $($items: tt)* - } - ) => { - $crate::$callback!( - @parse, - meta = { - lifetimes = [], - name = $name, - $(ctx = $ctxt,)* - $(main_self = $mainself,)* - outname = {$outname}, - scalar = {$scalar}, - }, - rest = $($items)* - ); - }; - - - ( - callback = $callback: ident, - rest = $name: ty $(: $ctxt: ty)* as $outname: tt $(| &$mainself:ident |)* { - $($items: tt)* - } - ) => { - $crate::$callback!( - @parse, - meta = { - lifetimes = [], - name = $name, - $(ctx = $ctxt,)* - $(main_self = $mainself,)* - outname = {$outname}, - scalar = {}, - }, - rest = $($items)* - ); - }; - - ( - callback = $callback: ident, - rest = <$($lifetime:tt),*> $name: ty $(: $ctxt: ty)* - where Scalar = <$generic:tt $(: $bound:tt)*> $(| &$mainself:ident |)* { - $($items: tt)* - } - ) => { - $crate::$callback!( - @parse, - meta = { - lifetimes = [$($lifetime,)*], - name = $name, - $(ctx = $ctxt,)* - $(main_self = $mainself,)* - outname = {stringify!($name)}, - scalar = {<$generic $(:$bounds)*>}, - }, - rest = $($items)* - ); - }; - - ( - callback = $callback: ident, - rest = <$($lifetime:tt),*> $name: ty $(: $ctxt: ty)* - where Scalar = $scalar: ty $(| &$mainself:ident |)* { - $($items: tt)* - } - ) => { - $crate::$callback!( - @parse, - meta = { - lifetimes = [$($lifetime,)*], - name = $name, - $(ctx = $ctxt,)* - $(main_self = $mainself,)* - outname = {stringify!($name)}, - scalar = {$scalar}, - }, - rest = $($items)* - ); - }; - - ( - callback = $callback: ident, - rest = <$($lifetime:tt),*> $name: ty $(: $ctxt: ty)* $(| &$mainself:ident |)* { - $($items: tt)* - } - ) => { - $crate::$callback!( - @parse, - meta = { - lifetimes = [$($lifetime,)*], - name = $name, - $(ctx = $ctxt,)* - $(main_self = $mainself,)* - outname = {stringify!($name)}, - scalar = {}, - }, - rest = $($items)* - ); - }; - - - ( - callback = $callback: ident, - rest = $name: ty $(: $ctxt: ty)* - where Scalar = <$generic:tt $(: $bound:tt)*> $(| &$mainself:ident |)* - { - $($items: tt)* - } - ) => { - $crate::$callback!( - @parse, - meta = { - lifetimes = [], - name = $name, - $(ctx = $ctxt,)* - $(main_self = $mainself,)* - outname = {stringify!($name)}, - scalar = {<$generic $(: $bound)*>}, - }, - rest = $($items)* - ); - }; - - ( - callback = $callback: ident, - rest = $name: ty $(: $ctxt: ty)* where Scalar = $scalar: ty $(| &$mainself:ident |)* { - $($items: tt)* - } - ) => { - $crate::$callback!( - @parse, - meta = { - lifetimes = [], - name = $name, - $(ctx = $ctxt,)* - $(main_self = $mainself,)* - outname = {stringify!($name)}, - scalar = {$scalar}, - }, - rest = $($items)* - ); - }; - - ( - callback = $callback: ident, - rest = $name: ty $(: $ctxt: ty)* $(| &$mainself:ident |)* { - $($items: tt)* - } - ) => { - $crate::$callback!( - @parse, - meta = { - lifetimes = [], - name = $name, - $(ctx = $ctxt,)* - $(main_self = $mainself,)* - outname = {stringify!($name)}, - scalar = {}, - }, - rest = $($items)* - ); - }; - ( - callback = $callback: ident, - rest = $($rest:tt)* - ) => { - compile_error!("Invalid syntax"); - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __juniper_parse_field_list { - ( - success_callback = $success_callback: ident, - additional_parser = {$($additional:tt)*}, - meta = {$($meta:tt)*}, - items = [$({$($items: tt)*},)*], - rest = - ) => { - $crate::$success_callback!( - @generate, - meta = {$($meta)*}, - items = [$({$($items)*},)*], - ); - }; - - ( - success_callback = $success_callback: ident, - additional_parser = {$($additional:tt)*}, - meta = {$($meta:tt)*}, - items = [$({$($items: tt)*},)*], - rest = , $($rest: tt)* - ) => { - $crate::__juniper_parse_field_list!( - success_callback = $success_callback, - additional_parser = {$($additional)*}, - meta = {$($meta)*}, - items = [$({$($items)*},)*], - rest = $($rest)* - ); - }; - - - ( - @parse_description, - success_callback = $success_callback: ident, - additional_parser = {$($additional:tt)*}, - meta = { - $(lifetimes = [$($lifetime:tt,)*],)* - $(name = $name:ty,)* - $(ctx = $ctxt: ty,)* - $(main_self = $mainself: ident,)* - $(outname = {$($outname:tt)*},)* - $(scalar = {$($scalar:tt)*},)* - $(description = $_desciption: tt,)* - $(additional = {$($other: tt)*},)* - }, - items = [$({$($items: tt)*},)*], - rest = $desc: tt $($rest:tt)* - ) => { - $crate::__juniper_parse_field_list!( - success_callback = $success_callback, - additional_parser = {$($additional)*}, - meta = { - $(lifetimes = [$($lifetime,)*],)* - $(name = $name,)* - $(ctx = $ctxt,)* - $(main_self = $mainself,)* - $(outname = {$($outname)*},)* - $(scalar = {$($scalar)*},)* - description = $desc, - $(additional = {$($other)*},)* - - }, - items = [$({$($items)*},)*], - rest = $($rest)* - ); - }; - ( - success_callback = $success_callback: ident, - additional_parser = {$($additional:tt)*}, - meta = { $($meta:tt)*}, - items = [$({$($items: tt)*},)*], - rest = description: $($rest:tt)* - ) => { - $crate::__juniper_parse_field_list!( - @parse_description, - success_callback = $success_callback, - additional_parser = {$($additional)*}, - meta = {$($meta)*}, - items = [$({$($items)*},)*], - rest = $($rest)* - ); - }; - - ( - success_callback = $success_callback: ident, - additional_parser = {$($additional:tt)*}, - meta = {$($meta:tt)*}, - items = [$({$($items: tt)*},)*], - rest = $(#[doc = $desc: tt])* - #[deprecated $(( $(since = $since: tt,)* note = $reason: tt ))* ] - field $name: ident ( - $(&$executor: tt)* $(,)* - $($(#[doc = $arg_desc: expr])* $arg_name:ident $(= $arg_default: tt)* : $arg_ty: ty),* $(,)* - ) -> $return_ty: ty $body: block - $($rest:tt)* - ) => { - $crate::__juniper_parse_field_list!( - success_callback = $success_callback, - additional_parser = {$($additional)*}, - meta = {$($meta)*}, - items = [$({$($items)*},)* { - name = $name, - body = $body, - return_ty = $return_ty, - args = [ - $({ - arg_name = $arg_name, - arg_ty = $arg_ty, - $(arg_default = $arg_default,)* - $(arg_docstring = $arg_desc,)* - },)* - ], - $(docstring = $desc,)* - deprecated = None$(.unwrap_or_else(|| Some($reason)))*, - $(executor_var = $executor,)* - },], - rest = $($rest)* - ); - }; - ( - success_callback = $success_callback: ident, - additional_parser = {$($additional:tt)*}, - meta = {$($meta:tt)*}, - items = [$({$($items: tt)*},)*], - rest = $(#[doc = $desc: tt])* - field $name: ident ( - $(&$executor: ident)* $(,)* - $($(#[doc = $arg_desc: expr])* $arg_name:ident $(= $arg_default: tt)* : $arg_ty: ty),* $(,)* - ) -> $return_ty: ty $body: block - $($rest:tt)* - ) => { - $crate::__juniper_parse_field_list!( - success_callback = $success_callback, - additional_parser = {$($additional)*}, - meta = {$($meta)*}, - items = [$({$($items)*},)* { - name = $name, - body = $body, - return_ty = $return_ty, - args = [ - $({ - arg_name = $arg_name, - arg_ty = $arg_ty, - $(arg_default = $arg_default,)* - $(arg_docstring = $arg_desc,)* - },)* - ], - $(docstring = $desc,)* - $(executor_var = $executor,)* - },], - rest = $($rest)* - ); - }; - ( - success_callback = $success_callback: ident, - additional_parser = {$($additional:tt)*}, - meta = {$($meta:tt)*}, - items = [$({$($items: tt)*},)*], - rest = field deprecated $reason:tt $name: ident ( - $(&$executor: tt)* $(,)* - $($arg_name:ident $(= $arg_default: tt)* : $arg_ty: ty $(as $arg_desc: expr)*),* $(,)* - ) -> $return_ty: ty $(as $desc: tt)* $body: block - $($rest:tt)* - ) => { - $crate::__juniper_parse_field_list!( - success_callback = $success_callback, - additional_parser = {$($additional)*}, - meta = {$($meta)*}, - items = [$({$($items)*},)* { - name = $name, - body = $body, - return_ty = $return_ty, - args = [ - $({ - arg_name = $arg_name, - arg_ty = $arg_ty, - $(arg_default = $arg_default,)* - $(arg_description = $arg_desc,)* - },)* - ], - $(decs = $desc,)* - deprecated = Some($reason), - $(executor_var = $executor,)* - },], - rest = $($rest)* - ); - }; - ( - success_callback = $success_callback: ident, - additional_parser = {$($additional:tt)*}, - meta = {$($meta:tt)*}, - items = [$({$($items: tt)*},)*], - rest = field $name: ident ( - $(&$executor: ident)* $(,)* - $($arg_name:ident $(= $arg_default: tt)* : $arg_ty: ty $(as $arg_desc: expr)*),* $(,)* - ) -> $return_ty: ty $(as $desc: tt)* $body: block - $($rest:tt)* - ) => { - $crate::__juniper_parse_field_list!( - success_callback = $success_callback, - additional_parser = {$($additional)*}, - meta = {$($meta)*}, - items = [$({$($items)*},)* { - name = $name, - body = $body, - return_ty = $return_ty, - args = [ - $({ - arg_name = $arg_name, - arg_ty = $arg_ty, - $(arg_default = $arg_default,)* - $(arg_description = $arg_desc,)* - },)* - ], - $(decs = $desc,)* - $(executor_var = $executor,)* - },], - rest = $($rest)* - ); - }; - - ( - success_callback = $success_callback: ident, - additional_parser = { - callback = $callback: ident, - header = {$($header:tt)*}, - }, - meta = {$($meta:tt)*}, - items = [$({$($items: tt)*},)*], - rest = $($rest:tt)* - ) => { - $crate::$callback!( - $($header)* - success_callback = $success_callback, - additional_parser = { - callback = $callback, - header = {$($header)*}, - }, - meta = {$($meta)*}, - items = [$({$($items)*},)*], - rest = $($rest)* - ); - } - -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __juniper_parse_instance_resolver { - ( - success_callback = $success_callback: ident, - additional_parser = {$($additional:tt)*}, - meta = { - lifetimes = [$($lifetime:tt,)*], - name = $name:ty, - ctx = $ctxt:ty, - main_self = $mainself:ident, - outname = {$($outname:tt)*}, - scalar = {$($scalar:tt)*}, - $(description = $desciption:tt,)* - $(additional = { - $(resolver = {$($ignored_resolver:tt)*},)* - },)* - - }, - items = [$({$($items: tt)*},)*], - rest = instance_resolvers: |&$context: ident| { - $( $srctype:ty => $resolver:expr ),* $(,)* - } $($rest:tt)* - ) => { - $crate::__juniper_parse_field_list!( - success_callback = $success_callback, - additional_parser = {$($additional)*}, - meta = { - lifetimes = [$($lifetime,)*], - name = $name, - ctx = $ctxt, - main_self = $mainself, - outname = {$($outname)*}, - scalar = {$($scalar)*}, - $(description = $desciption,)* - additional = { - resolver = { - context = $context, - items = [ - $({ - src = $srctype, - resolver = $resolver, - },)* - ], - }, - }, - }, - items = [$({$($items)*},)*], - rest = $($rest)* - ); - }; - - ( - success_callback = $success_callback: ident, - additional_parser = {$($additional:tt)*}, - meta = { - lifetimes = [$($lifetime:tt,)*], - name = $name:ty, - ctx = $ctxt:ty, - main_self = $mainself:ident, - outname = {$($outname:tt)*}, - scalar = {$($scalar:tt)*}, - $(description = $desciption:tt,)* - $(additional = { - $(resolver = {$($ignored_resolver:tt)*},)* - },)* - - }, - items = [$({$($items: tt)*},)*], - rest = instance_resolvers: |$(&)* _| {$( $srctype:ty => $resolver:expr ),* $(,)*} $($rest:tt)* - ) => { - $crate::__juniper_parse_field_list!( - success_callback = $success_callback, - additional_parser = {$($additional)*}, - meta = { - lifetimes = [$($lifetime,)*], - name = $name, - ctx = $ctxt, - main_self = $mainself, - outname = {$($outname)*}, - scalar = {$($scalar)*}, - $(description = $desciption,)* - additional = { - resolver = { - items = [ - $({ - src = $srctype, - resolver = $resolver, - },)* - ], - }, - }, - }, - items = [$({$($items)*},)*], - rest = $($rest)* - ); - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __juniper_create_arg { - ( - registry = $reg: ident, - info = $info: ident, - arg_ty = $arg_ty: ty, - arg_name = $arg_name: ident, - $(description = $arg_description: expr,)* - $(docstring = $arg_docstring: expr,)* - ) => { - $reg.arg::<$arg_ty>( - &$crate::to_camel_case(stringify!($arg_name)), - $info, - ) - $(.description($arg_description))* - .push_docstring(&[$($arg_docstring,)*]) - }; - - ( - registry = $reg: ident, - info = $info: ident, - arg_ty = $arg_ty: ty, - arg_name = $arg_name: ident, - default = $arg_default: expr, - $(description = $arg_description: expr,)* - $(docstring = $arg_docstring: expr,)* - ) => { - $reg.arg_with_default::<$arg_ty>( - &$crate::to_camel_case(stringify!($arg_name)), - &($arg_default), - $info, - ) - $(.description($arg_description))* - .push_docstring(&[$($arg_docstring,)*]) - }; -} diff --git a/juniper/src/macros/helper/mod.rs b/juniper/src/macros/helper/mod.rs new file mode 100644 index 00000000..8162b7a9 --- /dev/null +++ b/juniper/src/macros/helper/mod.rs @@ -0,0 +1,34 @@ +//! Helper traits and definitions for macros. + +pub mod subscription; + +use crate::{DefaultScalarValue, DynGraphQLValue, DynGraphQLValueAsync, ScalarValue}; + +/// Conversion of a [`GraphQLValue`] to its [trait object][1]. +/// +/// [`GraphQLValue`]: crate::GraphQLValue +/// [1]: https://doc.rust-lang.org/reference/types/trait-object.html +pub trait AsDynGraphQLValue { + /// Context type of this [`GraphQLValue`]. + /// + /// [`GraphQLValue`]: crate::GraphQLValue + type Context; + + /// Schema information type of this [`GraphQLValue`]. + /// + /// [`GraphQLValue`]: crate::GraphQLValue + type TypeInfo; + + /// Converts this value to a [`DynGraphQLValue`] [trait object][1]. + /// + /// [1]: https://doc.rust-lang.org/reference/types/trait-object.html + fn as_dyn_graphql_value(&self) -> &DynGraphQLValue; + + /// Converts this value to a [`DynGraphQLValueAsync`] [trait object][1]. + /// + /// [1]: https://doc.rust-lang.org/reference/types/trait-object.html + fn as_dyn_graphql_value_async(&self) + -> &DynGraphQLValueAsync; +} + +crate::sa::assert_obj_safe!(AsDynGraphQLValue); diff --git a/juniper/src/macros/subscription_helpers.rs b/juniper/src/macros/helper/subscription.rs similarity index 100% rename from juniper/src/macros/subscription_helpers.rs rename to juniper/src/macros/helper/subscription.rs diff --git a/juniper/src/macros/interface.rs b/juniper/src/macros/interface.rs deleted file mode 100644 index bca44882..00000000 --- a/juniper/src/macros/interface.rs +++ /dev/null @@ -1,302 +0,0 @@ -/** -Expose GraphQL interfaces - -Mapping interfaces to GraphQL can be tricky: there is no direct counterpart to -GraphQL interfaces in Rust, and downcasting is not possible in the general case. -Many other GraphQL implementations in other languages use instance checks and -either dynamic typing or forced downcasts to support these features. - -A GraphQL interface defines fields that the implementing types also need to -implement. A GraphQL interface also needs to be able to determine the concrete -type name as well as downcast the general type to the actual concrete type. - -## Syntax - -See the documentation for [`graphql_object!`][1] on the general item and type -syntax. `graphql_interface!` requires an additional `instance_resolvers` item, -and does _not_ support the `interfaces` item. - -`instance_resolvers` is a match like structure used to resolve the concrete -instance type of the interface. It starts with a context argument and continues -with a number of match arms; on the left side is the indicated type, and on the -right an expression that resolve into `Option` of the type indicated: - -```rust,ignore -instance_resolvers: |&context| { - &Human => context.get_human(self.id()), // returns Option<&Human> - &Droid => context.get_droid(self.id()), // returns Option<&Droid> -}, -``` - -This is used for both the `__typename` field and when resolving a specialized -fragment, e.g. `...on Human`. For `__typename`, the resolvers will be executed -in order - the first one returning `Some` will be the determined type name. When -resolving fragment type conditions, only the corresponding match arm will be -executed. - -## Example - -A simplified extract from the StarWars schema example shows how to use the -shared context to implement downcasts. - -```rust -# extern crate juniper; -# use std::collections::HashMap; -struct Human { id: String } -struct Droid { id: String } -struct Database { - humans: HashMap, - droids: HashMap, -} - -trait Character { - fn id(&self) -> &str; -} - -impl Character for Human { - fn id(&self) -> &str { &self.id } -} - -impl Character for Droid { - fn id(&self) -> &str { &self.id } -} - -#[juniper::graphql_object(Context = Database)] -impl Human { - fn id(&self) -> &str { &self.id } -} - -#[juniper::graphql_object( - name = "Droid", - Context = Database, -)] -impl Droid { - fn id(&self) -> &str { &self.id } -} - -// You can introduce lifetimes or generic parameters by < > before the name. -juniper::graphql_interface!(<'a> &'a dyn Character: Database as "Character" |&self| { - field id() -> &str { self.id() } - - instance_resolvers: |&context| { - &Human => context.humans.get(self.id()), - &Droid => context.droids.get(self.id()), - } -}); - -# fn main() { } -``` - -[1]: macro.graphql_object!.html - -*/ -#[macro_export] -macro_rules! graphql_interface { - - ( - @generate, - meta = { - lifetimes = [$($lifetimes:tt,)*], - name = $name:ty, - ctx = $ctx:ty, - main_self = $main_self:ident, - outname = {$($outname:tt)*}, - scalar = {$($scalar:tt)*}, - $(description = $desciption:tt,)* - additional = { - resolver = { - $(context = $resolver_ctx: ident,)* - items = [ - $({ - src = $resolver_src: ty, - resolver = $resolver_expr: expr, - },)* - ], - }, - }, - }, - items = [$({ - name = $fn_name: ident, - body = $body: block, - return_ty = $return_ty: ty, - args = [$({ - arg_name = $arg_name : ident, - arg_ty = $arg_ty: ty, - $(arg_default = $arg_default: expr,)* - $(arg_description = $arg_description: expr,)* - $(arg_docstring = $arg_docstring: expr,)* - },)*], - $(decs = $fn_description: expr,)* - $(docstring = $docstring: expr,)* - $(deprecated = $deprecated: expr,)* - $(executor_var = $executor: ident,)* - },)*], - ) => { - $crate::__juniper_impl_trait!( - impl<$($scalar)* $(, $lifetimes)* > GraphQLType for $name { - fn name(_ : &Self::TypeInfo) -> Option<&'static str> { - Some($($outname)*) - } - - fn meta<'r>( - info: &Self::TypeInfo, - registry: &mut $crate::Registry<'r, $crate::__juniper_insert_generic!($($scalar)+)> - ) -> $crate::meta::MetaType<'r, $crate::__juniper_insert_generic!($($scalar)+)> - where - $crate::__juniper_insert_generic!($($scalar)+): 'r - { - // Ensure all child types are registered - $( - let _ = registry.get_type::<$resolver_src>(info); - )* - let fields = &[$( - registry.field_convert::<$return_ty, _, Self::Context>( - &$crate::to_camel_case(stringify!($fn_name)), - info - ) - $(.description($fn_description))* - .push_docstring(&[$($docstring,)*]) - $(.deprecated($deprecated))* - $(.argument( - $crate::__juniper_create_arg!( - registry = registry, - info = info, - arg_ty = $arg_ty, - arg_name = $arg_name, - $(default = $arg_default,)* - $(description = $arg_description,)* - $(docstring = $arg_docstring,)* - ) - ))*, - )*]; - registry.build_interface_type::<$name>( - info, fields - ) - $(.description($desciption))* - .into_meta() - } - } - ); - - $crate::__juniper_impl_trait!( - impl<$($scalar)* $(, $lifetimes)* > IsOutputType for $name { } - ); - - $crate::__juniper_impl_trait!( - impl<$($scalar)* $(, $lifetimes)* > GraphQLValue for $name { - type Context = $ctx; - type TypeInfo = (); - - fn type_name(&self, _ : &Self::TypeInfo) -> Option<&'static str> { - Some($($outname)*) - } - - #[allow(unused_variables)] - fn resolve_field( - &$main_self, - info: &Self::TypeInfo, - field: &str, - args: &$crate::Arguments<$crate::__juniper_insert_generic!($($scalar)+)>, - executor: &$crate::Executor - ) -> $crate::ExecutionResult<$crate::__juniper_insert_generic!($($scalar)+)> { - $( - if field == &$crate::to_camel_case(stringify!($fn_name)) { - let f = (|| { - $( - let $arg_name: $arg_ty = args.get(&$crate::to_camel_case(stringify!($arg_name))) - .expect(concat!( - "Argument ", - stringify!($arg_name), - " missing - validation must have failed" - )); - )* - $( - let $executor = &executor; - )* - $body - }); - let result: $return_ty = f(); - - return $crate::IntoResolvable::into(result, executor.context()) - .and_then(|res| { - match res { - Some((ctx, r)) => { - executor.replaced_context(ctx) - .resolve_with_ctx(&(), &r) - } - None => Ok($crate::Value::null()) - } - }); - } - )* - - panic!("Field {} not found on type {}", field, $($outname)*) - } - - #[allow(unused_variables)] - fn concrete_type_name(&$main_self, context: &Self::Context, _info: &Self::TypeInfo) -> String { - $(let $resolver_ctx = &context;)* - - $( - if ($resolver_expr as ::std::option::Option<$resolver_src>).is_some() { - return - <$resolver_src as $crate::GraphQLType<_>>::name(&()).unwrap().to_owned(); - } - )* - - panic!("Concrete type not handled by instance resolvers on {}", $($outname)*); - } - - fn resolve_into_type( - &$main_self, - _info: &Self::TypeInfo, - type_name: &str, - _: Option<&[$crate::Selection<$crate::__juniper_insert_generic!($($scalar)*)>]>, - executor: &$crate::Executor, - ) -> $crate::ExecutionResult<$crate::__juniper_insert_generic!($($scalar)*)> { - $(let $resolver_ctx = &executor.context();)* - - $( - if type_name == (<$resolver_src as $crate::GraphQLType<_>>::name(&())).unwrap() { - return executor.resolve(&(), &$resolver_expr); - } - )* - - panic!("Concrete type not handled by instance resolvers on {}", $($outname)*); - } - } - ); - }; - - ( - @parse, - meta = {$($meta:tt)*}, - rest = $($rest:tt)* - ) => { - $crate::__juniper_parse_field_list!( - success_callback = graphql_interface, - additional_parser = { - callback = __juniper_parse_instance_resolver, - header = {}, - }, - meta = {$($meta)*}, - items = [], - rest = $($rest)* - ); - }; - - (@$($stuff:tt)*) => { - compile_error!("Invalid syntax for `graphql_interface!`"); - }; - - ( - $($rest:tt)* - ) => { - $crate::__juniper_parse_object_header!( - callback = graphql_interface, - rest = $($rest)* - ); - } - - -} diff --git a/juniper/src/macros/mod.rs b/juniper/src/macros/mod.rs index 1d4c5700..b38958da 100644 --- a/juniper/src/macros/mod.rs +++ b/juniper/src/macros/mod.rs @@ -1,12 +1,6 @@ -// Wrapper macros which allows built-in macros to be recognized as "crate-local" -// and helper traits for #[juniper::graphql_subscription] macro. +//! Helper definitions for macros. -#[macro_use] -mod common; -#[macro_use] -mod interface; +pub mod helper; #[cfg(test)] mod tests; - -pub mod subscription_helpers; diff --git a/juniper/src/macros/tests/field.rs b/juniper/src/macros/tests/field.rs index cffa7dcf..1104454f 100644 --- a/juniper/src/macros/tests/field.rs +++ b/juniper/src/macros/tests/field.rs @@ -1,15 +1,3 @@ -use crate::{ - ast::InputValue, - executor::FieldResult, - schema::model::RootNode, - types::scalars::{EmptyMutation, EmptySubscription}, - value::{DefaultScalarValue, Object, Value}, -}; - -struct Interface; -#[derive(Debug)] -struct Root; - /* Syntax to validate: @@ -22,9 +10,21 @@ Syntax to validate: */ -#[crate::graphql_object( - interfaces = [&Interface], -)] +#![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(interfaces = [InterfaceValue])] impl Root { fn simple() -> i32 { 0 @@ -104,44 +104,85 @@ impl Root { } } -graphql_interface!(Interface: () |&self| { - field simple() -> i32 { 0 } +#[graphql_interface(scalar = DefaultScalarValue)] +impl Interface for Root { + fn simple(&self) -> i32 { + 0 + } - field description() -> i32 as "Field description" { 0 } + fn description(&self) -> i32 { + 0 + } - field deprecated "Deprecation reason" - deprecated() -> i32 { 0 } + fn deprecated(&self) -> i32 { + 0 + } - field deprecated "Deprecation reason" - deprecated_descr() -> i32 as "Field description" { 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, scalar = DefaultScalarValue)] +trait Interface { + fn simple(&self) -> i32; + + #[graphql_interface(desc = "Field description")] + fn description(&self) -> i32; + + #[graphql_interface(deprecated = "Deprecation reason")] + fn deprecated(&self) -> i32; + + #[graphql_interface(desc = "Field description", deprecated = "Deprecation reason")] + fn deprecated_descr(&self) -> i32; /// Field description - field attr_description() -> i32 { 0 } + fn attr_description(&self) -> i32; /// Field description /// with `collapse_docs` behavior - field attr_description_collapse() -> i32 { 0 } + 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. - field attr_description_long() -> i32 { 0 } + fn attr_description_long(&self) -> i32; #[deprecated] - field attr_deprecated() -> i32 { 0 } + fn attr_deprecated(&self) -> i32; #[deprecated(note = "Deprecation reason")] - field attr_deprecated_reason() -> i32 { 0 } + fn attr_deprecated_reason(&self) -> i32; /// Field description #[deprecated(note = "Deprecation reason")] - field attr_deprecated_descr() -> i32 { 0 } - - instance_resolvers: |&_| { - Root => Some(Root {}), - } -}); + fn attr_deprecated_descr(&self) -> i32; +} async fn run_field_info_query(type_name: &str, field_name: &str, f: F) where diff --git a/juniper/src/macros/tests/impl_subscription.rs b/juniper/src/macros/tests/impl_subscription.rs index d802a9d8..de56473c 100644 --- a/juniper/src/macros/tests/impl_subscription.rs +++ b/juniper/src/macros/tests/impl_subscription.rs @@ -17,7 +17,7 @@ struct WithLifetime<'a> { value: &'a str, } -#[crate::graphql_object(Context=Context)] +#[crate::graphql_object(Context = Context)] impl<'a> WithLifetime<'a> { fn value(&'a self) -> &'a str { self.value @@ -26,7 +26,7 @@ impl<'a> WithLifetime<'a> { struct WithContext; -#[crate::graphql_object(Context=Context)] +#[crate::graphql_object(Context = Context)] impl WithContext { fn ctx(ctx: &Context) -> bool { ctx.flag1 diff --git a/juniper/src/macros/tests/interface.rs b/juniper/src/macros/tests/interface.rs index 3e45f3cb..fffeded6 100644 --- a/juniper/src/macros/tests/interface.rs +++ b/juniper/src/macros/tests/interface.rs @@ -1,12 +1,3 @@ -use std::marker::PhantomData; - -use crate::{ - ast::InputValue, - schema::model::RootNode, - types::scalars::{EmptyMutation, EmptySubscription}, - value::{DefaultScalarValue, Object, Value}, -}; - /* Syntax to validate: @@ -19,135 +10,86 @@ Syntax to validate: */ +use crate::{ + ast::InputValue, + graphql_interface, graphql_object, + schema::model::RootNode, + types::scalars::{EmptyMutation, EmptySubscription}, + value::{DefaultScalarValue, Object, Value}, +}; + struct Concrete; -struct CustomName; - -#[allow(dead_code)] -struct WithLifetime<'a> { - data: PhantomData<&'a i32>, -} - -#[allow(dead_code)] -struct WithGenerics { - data: T, -} - -struct DescriptionFirst; -struct FieldsFirst; -struct InterfacesFirst; - -struct CommasWithTrailing; -struct CommasOnMeta; - -struct ResolversWithTrailingComma; - -struct Root; - -#[crate::graphql_object] +#[graphql_object(impl = [ + CustomNameValue, DescriptionValue, WithLifetimeValue<'_>, WithGenericsValue<()>, +])] impl Concrete { fn simple() -> i32 { 0 } } -graphql_interface!(CustomName: () as "ACustomNamedInterface" |&self| { - field simple() -> i32 { 0 } +#[graphql_interface(for = Concrete, name = "ACustomNamedInterface", scalar = DefaultScalarValue)] +trait CustomName { + fn simple(&self) -> i32; +} +#[graphql_interface(scalar = DefaultScalarValue)] +impl CustomName for Concrete { + fn simple(&self) -> i32 { + 0 + } +} - instance_resolvers: |_| { Concrete => Some(Concrete) } -}); +#[graphql_interface(for = Concrete, scalar = DefaultScalarValue)] +trait WithLifetime<'a> { + fn simple(&self) -> i32; +} +#[graphql_interface(scalar = DefaultScalarValue)] +impl<'a> WithLifetime<'a> for Concrete { + fn simple(&self) -> i32 { + 0 + } +} -graphql_interface!(<'a> WithLifetime<'a>: () as "WithLifetime" |&self| { - field simple() -> i32 { 0 } - instance_resolvers: |_| { Concrete => Some(Concrete) } -}); +#[graphql_interface(for = Concrete, scalar = DefaultScalarValue)] +trait WithGenerics { + fn simple(&self) -> i32; +} +#[graphql_interface(scalar = DefaultScalarValue)] +impl WithGenerics for Concrete { + fn simple(&self) -> i32 { + 0 + } +} -graphql_interface!( WithGenerics: () as "WithGenerics" |&self| { - field simple() -> i32 { 0 } - instance_resolvers: |_| { Concrete => Some(Concrete) } -}); +#[graphql_interface(for = Concrete, desc = "A description", scalar = DefaultScalarValue)] +trait Description { + fn simple(&self) -> i32; +} +#[graphql_interface(scalar = DefaultScalarValue)] +impl Description for Concrete { + fn simple(&self) -> i32 { + 0 + } +} -graphql_interface!(DescriptionFirst: () |&self| { - description: "A description" +struct Root; - field simple() -> i32 { 0 } - - instance_resolvers: |_| { Concrete => Some(Concrete) } -}); - -graphql_interface!(FieldsFirst: () |&self| { - field simple() -> i32 { 0 } - - description: "A description" - - instance_resolvers: |_| { Concrete => Some(Concrete) } -}); - -graphql_interface!(InterfacesFirst: () |&self| { - instance_resolvers: |_| { Concrete => Some(Concrete) } - - field simple() -> i32 { 0 } - - description: "A description" -}); - -graphql_interface!(CommasWithTrailing: () |&self| { - instance_resolvers: |_| { Concrete => Some(Concrete) }, - - field simple() -> i32 { 0 }, - - description: "A description", -}); - -graphql_interface!(CommasOnMeta: () |&self| { - instance_resolvers: |_| { Concrete => Some(Concrete) } - description: "A description", - - field simple() -> i32 { 0 } -}); - -graphql_interface!(ResolversWithTrailingComma: () |&self| { - instance_resolvers: |_| { Concrete => Some(Concrete), } - description: "A description", - - field simple() -> i32 { 0 } -}); - -#[crate::graphql_object( - // FIXME: make async work - noasync -)] -impl<'a> Root { - fn custom_name() -> CustomName { - CustomName {} +#[crate::graphql_object] +impl Root { + fn custom_name() -> CustomNameValue { + Concrete.into() } - fn with_lifetime() -> WithLifetime<'a> { - WithLifetime { data: PhantomData } + fn with_lifetime() -> WithLifetimeValue<'static> { + Concrete.into() } - fn with_generics() -> WithGenerics { - WithGenerics { data: 123 } + fn with_generics() -> WithGenericsValue { + Concrete.into() } - fn description_first() -> DescriptionFirst { - DescriptionFirst {} - } - fn fields_first() -> FieldsFirst { - FieldsFirst {} - } - fn interfaces_first() -> InterfacesFirst { - InterfacesFirst {} - } - - fn commas_with_trailing() -> CommasWithTrailing { - CommasWithTrailing {} - } - fn commas_on_meta() -> CommasOnMeta { - CommasOnMeta {} - } - - fn resolvers_with_trailing_comma() -> ResolversWithTrailingComma { - ResolversWithTrailingComma {} + fn description() -> DescriptionValue { + Concrete.into() } } @@ -256,115 +198,10 @@ async fn introspect_with_generics() { #[tokio::test] async fn introspect_description_first() { - run_type_info_query("DescriptionFirst", |object, fields| { + run_type_info_query("Description", |object, fields| { assert_eq!( object.get_field_value("name"), - Some(&Value::scalar("DescriptionFirst")) - ); - 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; -} - -#[tokio::test] -async fn introspect_fields_first() { - run_type_info_query("FieldsFirst", |object, fields| { - assert_eq!( - object.get_field_value("name"), - Some(&Value::scalar("FieldsFirst")) - ); - 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; -} - -#[tokio::test] -async fn introspect_interfaces_first() { - run_type_info_query("InterfacesFirst", |object, fields| { - assert_eq!( - object.get_field_value("name"), - Some(&Value::scalar("InterfacesFirst")) - ); - 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; -} - -#[tokio::test] -async fn introspect_commas_with_trailing() { - run_type_info_query("CommasWithTrailing", |object, fields| { - assert_eq!( - object.get_field_value("name"), - Some(&Value::scalar("CommasWithTrailing")) - ); - 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; -} - -#[tokio::test] -async fn introspect_commas_on_meta() { - run_type_info_query("CommasOnMeta", |object, fields| { - assert_eq!( - object.get_field_value("name"), - Some(&Value::scalar("CommasOnMeta")) - ); - 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; -} - -#[tokio::test] -async fn introspect_resolvers_with_trailing_comma() { - run_type_info_query("ResolversWithTrailingComma", |object, fields| { - assert_eq!( - object.get_field_value("name"), - Some(&Value::scalar("ResolversWithTrailingComma")) + Some(&Value::scalar("Description")) ); assert_eq!( object.get_field_value("description"), diff --git a/juniper/src/schema/meta.rs b/juniper/src/schema/meta.rs index 1f610ed6..be64e06f 100644 --- a/juniper/src/schema/meta.rs +++ b/juniper/src/schema/meta.rs @@ -639,29 +639,6 @@ impl<'a, S> Field<'a, S> { self } - /// Adds a (multi)line doc string to the description of the field. - /// Any leading or trailing newlines will be removed. - /// - /// If the docstring contains newlines, repeated leading tab and space characters - /// will be removed from the beginning of each line. - /// - /// If the description hasn't been set, the description is set to the provided line. - /// Otherwise, the doc string is added to the current description after a newline. - pub fn push_docstring(mut self, multiline: &[&str]) -> Field<'a, S> { - if let Some(docstring) = clean_docstring(multiline) { - match &mut self.description { - &mut Some(ref mut desc) => { - desc.push('\n'); - desc.push_str(&docstring); - } - desc @ &mut None => { - *desc = Some(docstring); - } - } - } - self - } - /// Add an argument to the field /// /// Arguments are unordered and can't contain duplicates by name. @@ -706,30 +683,9 @@ impl<'a, S> Argument<'a, S> { self } - /// Adds a (multi)line doc string to the description of the field. - /// Any leading or trailing newlines will be removed. - /// - /// If the docstring contains newlines, repeated leading tab and space characters - /// will be removed from the beginning of each line. - /// - /// If the description hasn't been set, the description is set to the provided line. - /// Otherwise, the doc string is added to the current description after a newline. - pub fn push_docstring(mut self, multiline: &[&str]) -> Argument<'a, S> { - if let Some(docstring) = clean_docstring(multiline) { - match &mut self.description { - &mut Some(ref mut desc) => { - desc.push('\n'); - desc.push_str(&docstring); - } - desc @ &mut None => *desc = Some(docstring), - } - } - self - } - /// Set the default value of the argument /// - /// This overwrites the description if any was previously set. + /// This overwrites the default value if any was previously set. pub fn default_value(mut self, default_value: InputValue) -> Self { self.default_value = Some(default_value); self @@ -798,35 +754,3 @@ where { >::from_input_value(v).is_some() } - -fn clean_docstring(multiline: &[&str]) -> Option { - if multiline.is_empty() { - return None; - } - let trim_start = multiline - .iter() - .filter_map(|ln| ln.chars().position(|ch| !ch.is_whitespace())) - .min() - .unwrap_or(0); - Some( - multiline - .iter() - .enumerate() - .flat_map(|(line, ln)| { - let new_ln = if !ln.chars().next().map(char::is_whitespace).unwrap_or(false) { - ln.trim_end() // skip trimming the first line - } else if ln.len() >= trim_start { - ln[trim_start..].trim_end() - } else { - "" - }; - new_ln.chars().chain( - ['\n'] - .iter() - .take_while(move |_| line < multiline.len() - 1) - .cloned(), - ) - }) - .collect::(), - ) -} diff --git a/juniper/src/tests/fixtures/starwars/mod.rs b/juniper/src/tests/fixtures/starwars/mod.rs index 2e271ab1..7207bd96 100644 --- a/juniper/src/tests/fixtures/starwars/mod.rs +++ b/juniper/src/tests/fixtures/starwars/mod.rs @@ -1,4 +1,3 @@ -pub mod model; pub mod schema; #[cfg(feature = "schema-language")] pub mod schema_language; diff --git a/juniper/src/tests/fixtures/starwars/model.rs b/juniper/src/tests/fixtures/starwars/model.rs deleted file mode 100644 index ae250607..00000000 --- a/juniper/src/tests/fixtures/starwars/model.rs +++ /dev/null @@ -1,319 +0,0 @@ -#![allow(missing_docs)] - -use std::collections::HashMap; - -use crate::GraphQLEnum; - -#[derive(GraphQLEnum, Copy, Clone, Eq, PartialEq, Debug)] -pub enum Episode { - #[graphql(name = "NEW_HOPE")] - NewHope, - Empire, - Jedi, -} - -pub trait Character { - fn id(&self) -> &str; - fn name(&self) -> &str; - fn friend_ids(&self) -> &[String]; - fn appears_in(&self) -> &[Episode]; - fn secret_backstory(&self) -> &Option; - fn as_character(&self) -> &dyn Character; -} - -pub trait Human: Character { - fn home_planet(&self) -> &Option; -} - -pub trait Droid: Character { - fn primary_function(&self) -> &Option; -} - -#[derive(Clone)] -struct HumanData { - id: String, - name: String, - friend_ids: Vec, - appears_in: Vec, - secret_backstory: Option, - home_planet: Option, -} - -#[derive(Clone)] -struct DroidData { - id: String, - name: String, - friend_ids: Vec, - appears_in: Vec, - secret_backstory: Option, - primary_function: Option, -} - -impl Character for HumanData { - fn id(&self) -> &str { - &self.id - } - fn name(&self) -> &str { - &self.name - } - fn friend_ids(&self) -> &[String] { - &self.friend_ids - } - fn appears_in(&self) -> &[Episode] { - &self.appears_in - } - fn secret_backstory(&self) -> &Option { - &self.secret_backstory - } - fn as_character(&self) -> &dyn Character { - self - } -} - -impl Human for HumanData { - fn home_planet(&self) -> &Option { - &self.home_planet - } -} - -impl Character for DroidData { - fn id(&self) -> &str { - &self.id - } - fn name(&self) -> &str { - &self.name - } - fn friend_ids(&self) -> &[String] { - &self.friend_ids - } - fn appears_in(&self) -> &[Episode] { - &self.appears_in - } - fn secret_backstory(&self) -> &Option { - &self.secret_backstory - } - fn as_character(&self) -> &dyn Character { - self - } -} - -impl Droid for DroidData { - fn primary_function(&self) -> &Option { - &self.primary_function - } -} - -#[derive(Default, Clone)] -pub struct Database { - humans: HashMap, - droids: HashMap, -} - -use crate::{ - executor::Registry, - schema::meta::MetaType, - types::base::{GraphQLType, GraphQLValue}, - value::ScalarValue, -}; - -impl GraphQLType for Database -where - S: ScalarValue, -{ - fn name(_: &()) -> Option<&str> { - Some("_Database") - } - - fn meta<'r>(_: &(), registry: &mut Registry<'r, S>) -> MetaType<'r, S> - where - S: 'r, - { - registry.build_object_type::(&(), &[]).into_meta() - } -} - -impl GraphQLValue for Database -where - S: ScalarValue, -{ - type Context = Self; - type TypeInfo = (); - - fn type_name<'i>(&self, info: &'i Self::TypeInfo) -> Option<&'i str> { - >::name(info) - } -} - -impl HumanData { - pub fn new( - id: &str, - name: &str, - friend_ids: &[&str], - appears_in: &[Episode], - secret_backstory: Option<&str>, - home_planet: Option<&str>, - ) -> HumanData { - HumanData { - id: id.to_owned(), - name: name.to_owned(), - friend_ids: friend_ids - .to_owned() - .into_iter() - .map(|f| f.to_owned()) - .collect(), - appears_in: appears_in.to_vec(), - secret_backstory: secret_backstory.map(|b| b.to_owned()), - home_planet: home_planet.map(|p| p.to_owned()), - } - } -} - -impl DroidData { - pub fn new( - id: &str, - name: &str, - friend_ids: &[&str], - appears_in: &[Episode], - secret_backstory: Option<&str>, - primary_function: Option<&str>, - ) -> DroidData { - DroidData { - id: id.to_owned(), - name: name.to_owned(), - friend_ids: friend_ids - .to_owned() - .into_iter() - .map(|f| f.to_owned()) - .collect(), - appears_in: appears_in.to_vec(), - secret_backstory: secret_backstory.map(|b| b.to_owned()), - primary_function: primary_function.map(|p| p.to_owned()), - } - } -} - -impl Database { - pub fn new() -> Database { - let mut humans = HashMap::new(); - let mut droids = HashMap::new(); - - humans.insert( - "1000".to_owned(), - HumanData::new( - "1000", - "Luke Skywalker", - &["1002", "1003", "2000", "2001"], - &[Episode::NewHope, Episode::Empire, Episode::Jedi], - None, - Some("Tatooine"), - ), - ); - - humans.insert( - "1001".to_owned(), - HumanData::new( - "1001", - "Darth Vader", - &["1004"], - &[Episode::NewHope, Episode::Empire, Episode::Jedi], - None, - Some("Tatooine"), - ), - ); - - humans.insert( - "1002".to_owned(), - HumanData::new( - "1002", - "Han Solo", - &["1000", "1003", "2001"], - &[Episode::NewHope, Episode::Empire, Episode::Jedi], - None, - None, - ), - ); - - humans.insert( - "1003".to_owned(), - HumanData::new( - "1003", - "Leia Organa", - &["1000", "1002", "2000", "2001"], - &[Episode::NewHope, Episode::Empire, Episode::Jedi], - None, - Some("Alderaan"), - ), - ); - - humans.insert( - "1004".to_owned(), - HumanData::new( - "1004", - "Wilhuff Tarkin", - &["1001"], - &[Episode::NewHope], - None, - None, - ), - ); - - droids.insert( - "2000".to_owned(), - DroidData::new( - "2000", - "C-3PO", - &["1000", "1002", "1003", "2001"], - &[Episode::NewHope, Episode::Empire, Episode::Jedi], - None, - Some("Protocol"), - ), - ); - - droids.insert( - "2001".to_owned(), - DroidData::new( - "2001", - "R2-D2", - &["1000", "1002", "1003"], - &[Episode::NewHope, Episode::Empire, Episode::Jedi], - None, - Some("Astromech"), - ), - ); - - Database { humans, droids } - } - - pub fn get_hero(&self, episode: Option) -> &dyn Character { - if episode == Some(Episode::Empire) { - self.get_human("1000").unwrap().as_character() - } else { - self.get_droid("2001").unwrap().as_character() - } - } - - pub fn get_human(&self, id: &str) -> Option<&dyn Human> { - self.humans.get(id).map(|h| h as &dyn Human) - } - - pub fn get_droid(&self, id: &str) -> Option<&dyn Droid> { - self.droids.get(id).map(|d| d as &dyn Droid) - } - - pub fn get_character(&self, id: &str) -> Option<&dyn Character> { - if let Some(h) = self.humans.get(id) { - Some(h) - } else if let Some(d) = self.droids.get(id) { - Some(d) - } else { - None - } - } - - pub fn get_friends(&self, c: &dyn Character) -> Vec<&dyn Character> { - c.friend_ids() - .iter() - .flat_map(|id| self.get_character(id)) - .collect() - } -} diff --git a/juniper/src/tests/fixtures/starwars/schema.rs b/juniper/src/tests/fixtures/starwars/schema.rs index e0818e57..8693f4b2 100644 --- a/juniper/src/tests/fixtures/starwars/schema.rs +++ b/juniper/src/tests/fixtures/starwars/schema.rs @@ -1,164 +1,385 @@ #![allow(missing_docs)] +use std::{collections::HashMap, pin::Pin}; + use crate::{ - executor::Context, - graphql_subscription, - tests::fixtures::starwars::model::{Character, Database, Droid, Episode, Human}, - GraphQLObject, + graphql_interface, graphql_object, graphql_subscription, Context, DefaultScalarValue, + GraphQLEnum, }; -use std::pin::Pin; - -impl Context for Database {} - -graphql_interface!(<'a> &'a dyn Character: Database as "Character" |&self| { - description: "A character in the Star Wars Trilogy" - - field id() -> &str as "The id of the character" { - self.id() - } - - field name() -> Option<&str> as "The name of the character" { - Some(self.name()) - } - - field friends(&executor) -> Vec<&dyn Character> - as "The friends of the character" { - executor.context().get_friends(self.as_character()) - } - - field appears_in() -> &[Episode] as "Which movies they appear in" { - self.appears_in() - } - - instance_resolvers: |&context| { - &dyn Human => context.get_human(&self.id()), - &dyn Droid => context.get_droid(&self.id()), - } -}); - -#[crate::graphql_object( - Context = Database, - Scalar = crate::DefaultScalarValue, - interfaces = [&dyn Character], - // FIXME: make async work - noasync -)] -/// A humanoid creature in the Star Wars universe. -impl<'a> &'a dyn Human { - /// The id of the human - fn id(&self) -> &str { - self.id() - } - - /// The name of the human - fn name(&self) -> Option<&str> { - Some(self.name()) - } - - /// The friends of the human - fn friends(&self, ctx: &Database) -> Vec<&dyn Character> { - ctx.get_friends(self.as_character()) - } - - /// Which movies they appear in - fn appears_in(&self) -> &[Episode] { - self.appears_in() - } - - /// The home planet of the human - fn home_planet(&self) -> &Option { - self.home_planet() - } -} - -#[crate::graphql_object( - Context = Database, - Scalar = crate::DefaultScalarValue, - interfaces = [&dyn Character], - // FIXME: make async work - noasync -)] -/// A mechanical creature in the Star Wars universe. -impl<'a> &'a dyn Droid { - /// The id of the droid - fn id(&self) -> &str { - self.id() - } - - /// The name of the droid - fn name(&self) -> Option<&str> { - Some(self.name()) - } - - /// The friends of the droid - fn friends(&self, ctx: &Database) -> Vec<&dyn Character> { - ctx.get_friends(self.as_character()) - } - - /// Which movies they appear in - fn appears_in(&self) -> &[Episode] { - self.appears_in() - } - - /// The primary function of the droid - fn primary_function(&self) -> &Option { - self.primary_function() - } -} pub struct Query; -#[crate::graphql_object( - Context = Database, - Scalar = crate::DefaultScalarValue, - // FIXME: make async work - noasync -)] +#[graphql_object(context = Database, scalar = DefaultScalarValue)] /// The root query object of the schema impl Query { #[graphql(arguments(id(description = "id of the human")))] - fn human(database: &Database, id: String) -> Option<&dyn Human> { + fn human(database: &Database, id: String) -> Option<&Human> { database.get_human(&id) } #[graphql(arguments(id(description = "id of the droid")))] - fn droid(database: &Database, id: String) -> Option<&dyn Droid> { + fn droid(database: &Database, 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" + 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<&dyn Character> { - Some(database.get_hero(episode).as_character()) + fn hero(database: &Database, episode: Option) -> Option { + Some(database.get_hero(episode)) } } -#[derive(GraphQLObject)] -#[graphql(description = "A humanoid creature in the Star Wars universe")] -#[derive(Clone)] -/// A humanoid creature in the Star Wars universe. -/// TODO: remove this when async interfaces are merged -struct HumanSubscription { - id: String, - name: String, - home_planet: String, -} - pub struct Subscription; -type HumanStream = Pin + Send>>; +type HumanStream = Pin + Send>>; #[graphql_subscription(context = Database)] /// Super basic subscription fixture impl Subscription { - async fn async_human() -> HumanStream { - Box::pin(futures::stream::once(async { - HumanSubscription { - id: "stream id".to_string(), - name: "stream name".to_string(), - home_planet: "stream home planet".to_string(), - } - })) + async fn async_human(context: &Database) -> HumanStream { + let human = context.get_human("1000").unwrap().clone(); + Box::pin(futures::stream::once(futures::future::ready(human))) + } +} +#[derive(GraphQLEnum, Clone, Copy, Debug, Eq, PartialEq)] +pub enum Episode { + #[graphql(name = "NEW_HOPE")] + NewHope, + Empire, + Jedi, +} + +#[graphql_interface(for = [Human, Droid], context = Database, scalar = DefaultScalarValue)] +/// A character in the Star Wars Trilogy +pub trait Character { + /// The id of the character + fn id(&self) -> &str; + + /// The name of the character + fn name(&self) -> Option<&str>; + + /// The friends of the character + fn friends(&self, ctx: &Database) -> Vec; + + /// Which movies they appear in + fn appears_in(&self) -> &[Episode]; + + #[graphql_interface(ignore)] + fn friends_ids(&self) -> &[String]; +} + +#[derive(Clone)] +pub struct Human { + id: String, + name: String, + friend_ids: Vec, + appears_in: Vec, + secret_backstory: Option, + home_planet: Option, +} + +impl Human { + pub fn new( + id: &str, + name: &str, + friend_ids: &[&str], + appears_in: &[Episode], + secret_backstory: Option<&str>, + home_planet: Option<&str>, + ) -> Self { + Self { + id: id.to_owned(), + name: name.to_owned(), + friend_ids: friend_ids + .to_owned() + .into_iter() + .map(ToOwned::to_owned) + .collect(), + appears_in: appears_in.to_vec(), + secret_backstory: secret_backstory.map(ToOwned::to_owned), + home_planet: home_planet.map(|p| p.to_owned()), + } + } +} + +/// A humanoid creature in the Star Wars universe. +#[graphql_object( + context = Database, + scalar = DefaultScalarValue, + interfaces = CharacterValue, +)] +impl Human { + /// The id of the human + fn id(&self) -> &str { + &self.id + } + + /// The name of the human + fn name(&self) -> Option<&str> { + Some(self.name.as_str()) + } + + /// The friends of the human + fn friends(&self, ctx: &Database) -> Vec { + ctx.get_friends(self) + } + + /// Which movies they appear in + fn appears_in(&self) -> &[Episode] { + &self.appears_in + } + + /// The home planet of the human + fn home_planet(&self) -> &Option { + &self.home_planet + } +} + +#[graphql_interface(scalar = DefaultScalarValue)] +impl Character for Human { + fn id(&self) -> &str { + &self.id + } + + fn name(&self) -> Option<&str> { + Some(&self.name) + } + + fn friends(&self, ctx: &Database) -> Vec { + ctx.get_friends(self) + } + + fn appears_in(&self) -> &[Episode] { + &self.appears_in + } + + fn friends_ids(&self) -> &[String] { + &self.friend_ids + } +} + +#[derive(Clone)] +pub struct Droid { + id: String, + name: String, + friend_ids: Vec, + appears_in: Vec, + secret_backstory: Option, + primary_function: Option, +} + +impl Droid { + pub fn new( + id: &str, + name: &str, + friend_ids: &[&str], + appears_in: &[Episode], + secret_backstory: Option<&str>, + primary_function: Option<&str>, + ) -> Self { + Self { + id: id.to_owned(), + name: name.to_owned(), + friend_ids: friend_ids + .to_owned() + .into_iter() + .map(ToOwned::to_owned) + .collect(), + appears_in: appears_in.to_vec(), + secret_backstory: secret_backstory.map(ToOwned::to_owned), + primary_function: primary_function.map(ToOwned::to_owned), + } + } +} + +/// A mechanical creature in the Star Wars universe. +#[graphql_object( + context = Database, + scalar = DefaultScalarValue, + interfaces = CharacterValue, +)] +impl Droid { + /// The id of the droid + fn id(&self) -> &str { + &self.id + } + + /// The name of the droid + fn name(&self) -> Option<&str> { + Some(self.name.as_str()) + } + + /// The friends of the droid + fn friends(&self, ctx: &Database) -> Vec { + ctx.get_friends(self) + } + + /// Which movies they appear in + fn appears_in(&self) -> &[Episode] { + &self.appears_in + } + + /// The primary function of the droid + fn primary_function(&self) -> &Option { + &self.primary_function + } +} + +#[graphql_interface(scalar = DefaultScalarValue)] +impl Character for Droid { + fn id(&self) -> &str { + &self.id + } + + fn name(&self) -> Option<&str> { + Some(&self.name) + } + + fn friends(&self, ctx: &Database) -> Vec { + ctx.get_friends(self) + } + + fn appears_in(&self) -> &[Episode] { + &self.appears_in + } + + fn friends_ids(&self) -> &[String] { + &self.friend_ids + } +} + +#[derive(Default, Clone)] +pub struct Database { + humans: HashMap, + droids: HashMap, +} + +impl Context for Database {} + +impl Database { + pub fn new() -> Database { + let mut humans = HashMap::new(); + let mut droids = HashMap::new(); + + humans.insert( + "1000".to_owned(), + Human::new( + "1000", + "Luke Skywalker", + &["1002", "1003", "2000", "2001"], + &[Episode::NewHope, Episode::Empire, Episode::Jedi], + None, + Some("Tatooine"), + ), + ); + + humans.insert( + "1001".to_owned(), + Human::new( + "1001", + "Darth Vader", + &["1004"], + &[Episode::NewHope, Episode::Empire, Episode::Jedi], + None, + Some("Tatooine"), + ), + ); + + humans.insert( + "1002".to_owned(), + Human::new( + "1002", + "Han Solo", + &["1000", "1003", "2001"], + &[Episode::NewHope, Episode::Empire, Episode::Jedi], + None, + None, + ), + ); + + humans.insert( + "1003".to_owned(), + Human::new( + "1003", + "Leia Organa", + &["1000", "1002", "2000", "2001"], + &[Episode::NewHope, Episode::Empire, Episode::Jedi], + None, + Some("Alderaan"), + ), + ); + + humans.insert( + "1004".to_owned(), + Human::new( + "1004", + "Wilhuff Tarkin", + &["1001"], + &[Episode::NewHope], + None, + None, + ), + ); + + droids.insert( + "2000".to_owned(), + Droid::new( + "2000", + "C-3PO", + &["1000", "1002", "1003", "2001"], + &[Episode::NewHope, Episode::Empire, Episode::Jedi], + None, + Some("Protocol"), + ), + ); + + droids.insert( + "2001".to_owned(), + Droid::new( + "2001", + "R2-D2", + &["1000", "1002", "1003"], + &[Episode::NewHope, Episode::Empire, Episode::Jedi], + None, + Some("Astromech"), + ), + ); + + Database { humans, droids } + } + + pub fn get_hero(&self, episode: Option) -> CharacterValue { + if episode == Some(Episode::Empire) { + self.get_human("1000").unwrap().clone().into() + } else { + self.get_droid("2001").unwrap().clone().into() + } + } + + pub fn get_human(&self, id: &str) -> Option<&Human> { + self.humans.get(id) + } + + pub fn get_droid(&self, id: &str) -> Option<&Droid> { + self.droids.get(id) + } + + pub fn get_character(&self, id: &str) -> Option { + if let Some(h) = self.humans.get(id) { + Some(h.clone().into()) + } else if let Some(d) = self.droids.get(id) { + Some(d.clone().into()) + } else { + None + } + } + + pub fn get_friends(&self, c: &dyn Character) -> Vec { + c.friends_ids() + .iter() + .flat_map(|id| self.get_character(id)) + .collect() } } diff --git a/juniper/src/tests/fixtures/starwars/schema_language.rs b/juniper/src/tests/fixtures/starwars/schema_language.rs index c2f16023..7a71c8d7 100644 --- a/juniper/src/tests/fixtures/starwars/schema_language.rs +++ b/juniper/src/tests/fixtures/starwars/schema_language.rs @@ -8,7 +8,8 @@ mod tests { use crate::{ schema::model::RootNode, tests::fixtures::starwars::{ - model::Database, schema::Query, schema_language::STATIC_GRAPHQL_SCHEMA_DEFINITION, + schema::{Database, Query}, + schema_language::STATIC_GRAPHQL_SCHEMA_DEFINITION, }, types::scalars::{EmptyMutation, EmptySubscription}, }; diff --git a/juniper/src/tests/introspection_tests.rs b/juniper/src/tests/introspection_tests.rs index 46b7a21c..38259eea 100644 --- a/juniper/src/tests/introspection_tests.rs +++ b/juniper/src/tests/introspection_tests.rs @@ -1,14 +1,15 @@ use std::collections::HashSet; -use super::schema_introspection::*; use crate::{ executor::Variables, introspection::IntrospectionFormat, schema::model::RootNode, - tests::fixtures::starwars::{model::Database, schema::Query}, + tests::fixtures::starwars::schema::{Database, Query}, types::scalars::{EmptyMutation, EmptySubscription}, }; +use super::schema_introspection::*; + #[tokio::test] async fn test_introspection_query_type_name() { let doc = r#" diff --git a/juniper/src/tests/query_tests.rs b/juniper/src/tests/query_tests.rs index c68e3a52..d671c5bf 100644 --- a/juniper/src/tests/query_tests.rs +++ b/juniper/src/tests/query_tests.rs @@ -2,7 +2,7 @@ use crate::{ ast::InputValue, executor::Variables, schema::model::RootNode, - tests::fixtures::starwars::{model::Database, schema::Query}, + tests::fixtures::starwars::schema::{Database, Query}, types::scalars::{EmptyMutation, EmptySubscription}, value::Value, }; @@ -641,8 +641,8 @@ async fn test_query_inline_fragments_droid() { "hero", Value::object( vec![ - ("name", Value::scalar("R2-D2")), ("__typename", Value::scalar("Droid")), + ("name", Value::scalar("R2-D2")), ("primaryFunction", Value::scalar("Astromech")), ] .into_iter() @@ -662,8 +662,8 @@ async fn test_query_inline_fragments_human() { let doc = r#" query InlineFragments { hero(episode: EMPIRE) { - name __typename + name } } "#; @@ -682,8 +682,8 @@ async fn test_query_inline_fragments_human() { "hero", Value::object( vec![ - ("name", Value::scalar("Luke Skywalker")), ("__typename", Value::scalar("Human")), + ("name", Value::scalar("Luke Skywalker")), ] .into_iter() .collect(), diff --git a/juniper/src/types/async_await.rs b/juniper/src/types/async_await.rs index 4151127d..395faf5e 100644 --- a/juniper/src/types/async_await.rs +++ b/juniper/src/types/async_await.rs @@ -113,9 +113,15 @@ where crate::sa::assert_obj_safe!(GraphQLValueAsync); +/// Helper alias for naming [trait objects][1] of [`GraphQLValueAsync`]. +/// +/// [1]: https://doc.rust-lang.org/reference/types/trait-object.html +pub type DynGraphQLValueAsync = + dyn GraphQLValueAsync + Send + 'static; + /// Extension of [`GraphQLType`] trait with asynchronous queries/mutations resolvers. /// -/// It's automatically implemented for [`GraphQLValueAsync`] and [`GraphQLType`] implementors, so +/// It's automatically implemented for [`GraphQLValueAsync`] and [`GraphQLType`] implementers, so /// doesn't require manual or code-generated implementation. pub trait GraphQLTypeAsync: GraphQLValueAsync + GraphQLType where diff --git a/juniper/src/types/base.rs b/juniper/src/types/base.rs index d7c77f73..46072ee6 100644 --- a/juniper/src/types/base.rs +++ b/juniper/src/types/base.rs @@ -290,6 +290,12 @@ where crate::sa::assert_obj_safe!(GraphQLValue); +/// Helper alias for naming [trait objects][1] of [`GraphQLValue`]. +/// +/// [1]: https://doc.rust-lang.org/reference/types/trait-object.html +pub type DynGraphQLValue = + dyn GraphQLValue + Send + Sync + 'static; + /// Primary trait used to expose Rust types in a GraphQL schema. /// /// All of the convenience macros ultimately expand into an implementation of diff --git a/juniper/src/types/marker.rs b/juniper/src/types/marker.rs index 79237741..4d95b761 100644 --- a/juniper/src/types/marker.rs +++ b/juniper/src/types/marker.rs @@ -23,13 +23,37 @@ pub trait GraphQLObjectType: GraphQLType { fn mark() {} } +/// Maker trait for [GraphQL interfaces][1]. +/// +/// This trait extends the [`GraphQLType`] and is only used to mark an [interface][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 interfaces][1]. Other types ([scalars][2], [enums][3], [objects][4], [input objects][5] +/// and [unions][6]) are not allowed. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Interfaces +/// [2]: https://spec.graphql.org/June2018/#sec-Scalars +/// [3]: https://spec.graphql.org/June2018/#sec-Enums +/// [4]: https://spec.graphql.org/June2018/#sec-Objects +/// [5]: https://spec.graphql.org/June2018/#sec-Input-Objects +/// [6]: https://spec.graphql.org/June2018/#sec-Unions +pub trait GraphQLInterface: GraphQLType { + /// An arbitrary function without meaning. + /// + /// 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() {} +} + /// Maker trait for [GraphQL unions][1]. /// -/// This trait extends the [`GraphQLType`] and is only used to mark [union][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 unions][1]. -/// Other types ([scalars][2], [enums][3], [objects][4], [input objects][5] and [interfaces][6]) are -/// not allowed. +/// This trait extends the [`GraphQLType`] and is only used to mark an [union][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 unions][1]. Other types ([scalars][2], [enums][3], [objects][4], [input objects][5] and +/// [interfaces][6]) are not allowed. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions /// [2]: https://spec.graphql.org/June2018/#sec-Scalars @@ -97,7 +121,7 @@ where { } -impl<'a, S, T> IsOutputType for &'a [T] +impl IsOutputType for [T] where T: IsOutputType, S: ScalarValue, @@ -111,7 +135,7 @@ where { } -impl<'a, S, T> IsInputType for &'a [T] +impl IsInputType for [T] where T: IsInputType, S: ScalarValue, @@ -120,13 +144,13 @@ where impl<'a, S, T> IsInputType for &T where - T: IsInputType, + T: IsInputType + ?Sized, S: ScalarValue, { } impl<'a, S, T> IsOutputType for &T where - T: IsOutputType, + T: IsOutputType + ?Sized, S: ScalarValue, { } @@ -144,5 +168,5 @@ where { } -impl<'a, S> IsInputType for &str where S: ScalarValue {} -impl<'a, S> IsOutputType for &str where S: ScalarValue {} +impl<'a, S> IsInputType for str where S: ScalarValue {} +impl<'a, S> IsOutputType for str where S: ScalarValue {} diff --git a/juniper/src/types/subscriptions.rs b/juniper/src/types/subscriptions.rs index 70662097..2b5981e4 100644 --- a/juniper/src/types/subscriptions.rs +++ b/juniper/src/types/subscriptions.rs @@ -201,7 +201,7 @@ crate::sa::assert_obj_safe!(GraphQLSubscriptionValue: diff --git a/juniper_actix/examples/actix_server.rs b/juniper_actix/examples/actix_server.rs index b3b7584c..ab32f315 100644 --- a/juniper_actix/examples/actix_server.rs +++ b/juniper_actix/examples/actix_server.rs @@ -5,7 +5,7 @@ use std::env; use actix_cors::Cors; use actix_web::{middleware, web, App, Error, HttpResponse, HttpServer}; use juniper::{ - tests::fixtures::starwars::{model::Database, schema::Query}, + tests::fixtures::starwars::schema::{Database, Query}, EmptyMutation, EmptySubscription, RootNode, }; use juniper_actix::{ diff --git a/juniper_actix/src/lib.rs b/juniper_actix/src/lib.rs index bfb86b27..5265e891 100644 --- a/juniper_actix/src/lib.rs +++ b/juniper_actix/src/lib.rs @@ -489,7 +489,7 @@ mod tests { use juniper::{ futures::stream::StreamExt, http::tests::{run_http_test_suite, HttpIntegration, TestResponse}, - tests::fixtures::starwars::{model::Database, schema::Query}, + tests::fixtures::starwars::schema::{Database, Query}, EmptyMutation, EmptySubscription, RootNode, }; @@ -666,7 +666,7 @@ mod tests { #[actix_web::rt::test] async fn batch_request_works() { use juniper::{ - tests::fixtures::starwars::{model::Database, schema::Query}, + tests::fixtures::starwars::schema::{Database, Query}, EmptyMutation, EmptySubscription, RootNode, }; diff --git a/juniper_codegen/src/common/gen.rs b/juniper_codegen/src/common/gen.rs new file mode 100644 index 00000000..dc5adc13 --- /dev/null +++ b/juniper_codegen/src/common/gen.rs @@ -0,0 +1,47 @@ +//! Common code generated parts, used by this crate. + +use proc_macro2::TokenStream; +use quote::quote; + +/// Generate the code resolving some [GraphQL type][1] in a synchronous manner. +/// +/// Value of a [GraphQL type][1] should be stored in a `res` binding in the generated code, before +/// including this piece of code. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Types +pub(crate) fn sync_resolving_code() -> TokenStream { + quote! { + ::juniper::IntoResolvable::into(res, executor.context()) + .and_then(|res| match res { + Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(info, &r), + None => Ok(::juniper::Value::null()), + }) + } +} + +/// Generate the code resolving some [GraphQL type][1] in an asynchronous manner. +/// +/// Value of a [GraphQL type][1] should be resolvable with `fut` binding representing a [`Future`] +/// in the generated code, before including this piece of code. +/// +/// Optional `ty` argument may be used to annotate a concrete type of the resolving +/// [GraphQL type][1] (the [`Future::Output`]). +/// +/// [`Future`]: std::future::Future +/// [`Future::Output`]: std::future::Future::Output +/// [1]: https://spec.graphql.org/June2018/#sec-Types +pub(crate) fn async_resolving_code(ty: Option<&syn::Type>) -> TokenStream { + let ty = ty.map(|t| quote! { : #t }); + + quote! { + Box::pin(::juniper::futures::FutureExt::then(fut, move |res #ty| async move { + match ::juniper::IntoResolvable::into(res, executor.context())? { + Some((ctx, r)) => { + let subexec = executor.replaced_context(ctx); + subexec.resolve_with_ctx_async(info, &r).await + }, + None => Ok(::juniper::Value::null()), + } + })) + } +} diff --git a/juniper_codegen/src/common/mod.rs b/juniper_codegen/src/common/mod.rs new file mode 100644 index 00000000..88498c1e --- /dev/null +++ b/juniper_codegen/src/common/mod.rs @@ -0,0 +1,89 @@ +//! Common functions, definitions and extensions for code generation, used by this crate. + +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) + } +} diff --git a/juniper_codegen/src/common/parse/attr.rs b/juniper_codegen/src/common/parse/attr.rs new file mode 100644 index 00000000..9e42cc97 --- /dev/null +++ b/juniper_codegen/src/common/parse/attr.rs @@ -0,0 +1,101 @@ +//! Common functions, definitions and extensions for parsing and modifying Rust attributes, used by +//! this crate. + +use proc_macro2::{Span, TokenStream}; +use syn::parse_quote; + +use crate::util::path_eq_single; + +/// Prepends the given `attrs` collection with a new [`syn::Attribute`] generated from the given +/// `attr_path` and `attr_args`. +/// +/// This function is generally used for uniting `proc_macro_attribute` with its body attributes. +pub(crate) fn unite( + (attr_path, attr_args): (&str, &TokenStream), + attrs: &[syn::Attribute], +) -> Vec { + let mut full_attrs = Vec::with_capacity(attrs.len() + 1); + let attr_path = syn::Ident::new(attr_path, Span::call_site()); + full_attrs.push(parse_quote! { #[#attr_path(#attr_args)] }); + full_attrs.extend_from_slice(attrs); + full_attrs +} + +/// Strips all `attr_path` attributes from the given `attrs` collection. +/// +/// This function is generally used for removing duplicate attributes during `proc_macro_attribute` +/// expansion, so avoid unnecessary expansion duplication. +pub(crate) fn strip(attr_path: &str, attrs: Vec) -> Vec { + attrs + .into_iter() + .filter(|attr| !path_eq_single(&attr.path, attr_path)) + .collect() +} + +/// Common errors of parsing Rust attributes, appeared in this crate. +pub(crate) mod err { + use proc_macro2::Span; + use syn::spanned::Spanned; + + /// Creates "duplicated argument" [`syn::Error`] for the given `name` pointing to the given + /// `span`. + #[must_use] + pub(crate) fn dup_arg(span: S) -> syn::Error { + syn::Error::new(span.as_span(), "duplicated attribute argument found") + } + + /// Creates "unknown argument" [`syn::Error`] for the given `name` pointing to the given `span`. + #[must_use] + pub(crate) fn unknown_arg(span: S, name: &str) -> syn::Error { + syn::Error::new( + span.as_span(), + format!("unknown `{}` attribute argument", name), + ) + } + + /// Helper coercion for [`Span`] and [`Spanned`] types to use in function arguments. + pub(crate) trait AsSpan { + /// Returns the coerced [`Span`]. + #[must_use] + fn as_span(&self) -> Span; + } + + impl AsSpan for Span { + #[inline] + fn as_span(&self) -> Self { + *self + } + } + + impl AsSpan for &T { + #[inline] + fn as_span(&self) -> Span { + self.span() + } + } +} + +/// Handy extension of [`Option`] methods, used in this crate. +pub(crate) trait OptionExt { + type Inner; + + /// Transforms the `Option` into a `Result<(), E>`, mapping `None` to `Ok(())` and `Some(v)` + /// to `Err(err(v))`. + fn none_or_else(self, err: F) -> Result<(), E> + where + F: FnOnce(Self::Inner) -> E; +} + +impl OptionExt for Option { + type Inner = T; + + fn none_or_else(self, err: F) -> Result<(), E> + where + F: FnOnce(T) -> E, + { + match self { + Some(v) => Err(err(v)), + None => Ok(()), + } + } +} diff --git a/juniper_codegen/src/common/parse/downcaster.rs b/juniper_codegen/src/common/parse/downcaster.rs new file mode 100644 index 00000000..89dd67cd --- /dev/null +++ b/juniper_codegen/src/common/parse/downcaster.rs @@ -0,0 +1,94 @@ +//! Common functions, definitions and extensions for parsing downcasting functions, used by GraphQL +//! [interfaces][1] and [unions][2] definitions to downcast its type to a concrete implementer type. +//! +//! [1]: https://spec.graphql.org/June2018/#sec-Interfaces +//! [2]: https://spec.graphql.org/June2018/#sec-Unions + +use proc_macro2::Span; +use syn::{ext::IdentExt as _, spanned::Spanned as _}; + +use crate::common::parse::TypeExt as _; + +/// Parses downcasting output type from the downcaster method return type. +/// +/// # Errors +/// +/// If return type is invalid (not `Option<&OutputType>`), then returns the [`Span`] to display the +/// corresponding error at. +pub(crate) fn output_type(ret_ty: &syn::ReturnType) -> Result { + let ret_ty = match &ret_ty { + syn::ReturnType::Type(_, ty) => &*ty, + _ => return Err(ret_ty.span()), + }; + + let path = match ret_ty.unparenthesized() { + syn::Type::Path(syn::TypePath { qself: None, path }) => path, + _ => return Err(ret_ty.span()), + }; + + let (ident, args) = match path.segments.last() { + Some(syn::PathSegment { + ident, + arguments: syn::PathArguments::AngleBracketed(generic), + }) => (ident, &generic.args), + _ => return Err(ret_ty.span()), + }; + + if ident.unraw() != "Option" { + return Err(ret_ty.span()); + } + if args.len() != 1 { + return Err(ret_ty.span()); + } + + let out_ty = match args.first() { + Some(syn::GenericArgument::Type(inner_ty)) => match inner_ty.unparenthesized() { + syn::Type::Reference(inner_ty) => { + if inner_ty.mutability.is_some() { + return Err(inner_ty.span()); + } + inner_ty.elem.unparenthesized().clone() + } + _ => return Err(ret_ty.span()), + }, + _ => return Err(ret_ty.span()), + }; + Ok(out_ty) +} + +/// Parses context type used for downcasting from the downcaster method signature. +/// +/// Returns [`None`] if downcaster method doesn't accept context. +/// +/// # Errors +/// +/// If input arguments are invalid, then returns the [`Span`] to display the corresponding error at. +pub(crate) fn context_ty(sig: &syn::Signature) -> Result, Span> { + match sig.receiver() { + Some(syn::FnArg::Receiver(rcv)) => { + if rcv.reference.is_none() || rcv.mutability.is_some() { + return Err(rcv.span()); + } + } + _ => return Err(sig.span()), + } + + if sig.inputs.len() > 2 { + return Err(sig.inputs.span()); + } + + let second_arg_ty = match sig.inputs.iter().nth(1) { + Some(syn::FnArg::Typed(arg)) => &*arg.ty, + None => return Ok(None), + _ => return Err(sig.inputs.span()), + }; + match second_arg_ty.unparenthesized() { + syn::Type::Reference(ref_ty) => { + if ref_ty.mutability.is_some() { + return Err(ref_ty.span()); + } + Ok(Some(ref_ty.elem.unparenthesized().clone())) + } + ty => Err(ty.span()), + } +} diff --git a/juniper_codegen/src/common/parse/mod.rs b/juniper_codegen/src/common/parse/mod.rs new file mode 100644 index 00000000..fce5f0cf --- /dev/null +++ b/juniper_codegen/src/common/parse/mod.rs @@ -0,0 +1,261 @@ +//! Common functions, definitions and extensions for parsing, normalizing and modifying Rust syntax, +//! used by this crate. + +pub(crate) mod attr; +pub(crate) mod downcaster; + +use std::{ + any::TypeId, + iter::{self, FromIterator as _}, + mem, +}; + +use proc_macro2::Span; +use syn::{ + ext::IdentExt as _, + parse::{Parse, ParseBuffer}, + parse_quote, + punctuated::Punctuated, + token::{self, Token}, +}; + +/// Extension of [`ParseBuffer`] providing common function widely used by this crate for parsing. +pub(crate) trait ParseBufferExt { + /// Tries to parse `T` as the next token. + /// + /// Doesn't move [`ParseStream`]'s cursor if there is no `T`. + fn try_parse(&self) -> syn::Result>; + + /// Checks whether next token is `T`. + /// + /// Doesn't move [`ParseStream`]'s cursor. + #[must_use] + fn is_next(&self) -> bool; + + /// Parses next token as [`syn::Ident`] _allowing_ Rust keywords, while default [`Parse`] + /// implementation for [`syn::Ident`] disallows keywords. + /// + /// Always moves [`ParseStream`]'s cursor. + fn parse_any_ident(&self) -> syn::Result; + + /// Checks whether next token is a wrapper `W` and if yes, then parses the wrapped tokens as `T` + /// [`Punctuated`] with `P`. Otherwise, parses just `T`. + /// + /// Always moves [`ParseStream`]'s cursor. + fn parse_maybe_wrapped_and_punctuated(&self) -> syn::Result> + where + T: Parse, + W: Default + Token + 'static, + P: Default + Parse + Token; +} + +impl<'a> ParseBufferExt for ParseBuffer<'a> { + fn try_parse(&self) -> syn::Result> { + Ok(if self.is_next::() { + Some(self.parse()?) + } else { + None + }) + } + + fn is_next(&self) -> bool { + self.lookahead1().peek(|_| T::default()) + } + + fn parse_any_ident(&self) -> syn::Result { + self.call(syn::Ident::parse_any) + } + + fn parse_maybe_wrapped_and_punctuated(&self) -> syn::Result> + where + T: Parse, + W: Default + Token + 'static, + P: Default + Parse + Token, + { + Ok(if self.is_next::() { + let inner; + if TypeId::of::() == TypeId::of::() { + let _ = syn::bracketed!(inner in self); + } else if TypeId::of::() == TypeId::of::() { + let _ = syn::braced!(inner in self); + } else if TypeId::of::() == TypeId::of::() { + let _ = syn::parenthesized!(inner in self); + } else { + unimplemented!( + "ParseBufferExt::parse_maybe_wrapped_and_punctuated supports only brackets, \ + braces and parentheses as wrappers.", + ); + } + Punctuated::parse_terminated(&inner)? + } else { + Punctuated::from_iter(iter::once(self.parse::()?)) + }) + } +} + +/// 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). + #[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. + /// + /// Also, makes the type [`TypeExt::unparenthesized`], if possible. + #[must_use] + fn unreferenced(&self) -> &Self; + + fn lifetimes_anonymized(&mut self); +} + +impl TypeExt for syn::Type { + fn unparenthesized(&self) -> &Self { + match self { + Self::Paren(ty) => ty.elem.unparenthesized(), + ty => ty, + } + } + + fn unreferenced(&self) -> &Self { + match self.unparenthesized() { + Self::Reference(ref_ty) => &*ref_ty.elem, + ty => ty, + } + } + + fn lifetimes_anonymized(&mut self) { + use syn::{GenericArgument as GA, Type as T}; + + 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::Tuple(syn::TypeTuple { elems, .. }) => { + for ty in elems.iter_mut() { + ty.lifetimes_anonymized(); + } + } + + T::ImplTrait(syn::TypeImplTrait { bounds, .. }) + | 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") + } + } + } + } + + T::Reference(ref_ty) => { + if let Some(lt) = ref_ty.lifetime.as_mut() { + lt.ident = syn::Ident::new("_", Span::call_site()); + } + (&mut *ref_ty.elem).lifetimes_anonymized(); + } + + 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 => {} + } + } + } + + // These types unlikely will be used as GraphQL types. + T::BareFn(_) + | T::Infer(_) + | T::Macro(_) + | T::Never(_) + | T::Verbatim(_) + | T::__Nonexhaustive => {} + } + } +} + +/// Extension of [`syn::Generics`] providing common function widely used by this crate for parsing. +pub(crate) trait GenericsExt { + /// Removes all default types out of type parameters and const parameters in these + /// [`syn::Generics`]. + fn remove_defaults(&mut self); + + /// Moves all trait and lifetime bounds of these [`syn::Generics`] to its [`syn::WhereClause`]. + fn move_bounds_to_where_clause(&mut self); +} + +impl GenericsExt for syn::Generics { + fn remove_defaults(&mut self) { + use syn::GenericParam as P; + + for p in &mut self.params { + match p { + P::Type(p) => { + p.eq_token = None; + p.default = None; + } + P::Lifetime(_) => {} + P::Const(p) => { + p.eq_token = None; + p.default = None; + } + } + } + } + + fn move_bounds_to_where_clause(&mut self) { + use syn::GenericParam as P; + + let _ = self.make_where_clause(); + let where_clause = self.where_clause.as_mut().unwrap(); + + for p in &mut self.params { + match p { + P::Type(p) => { + if p.colon_token.is_some() { + p.colon_token = None; + let bounds = mem::take(&mut p.bounds); + let ty = &p.ident; + where_clause.predicates.push(parse_quote! { #ty: #bounds }); + } + } + P::Lifetime(p) => { + if p.colon_token.is_some() { + p.colon_token = None; + let bounds = mem::take(&mut p.bounds); + let lt = &p.lifetime; + where_clause.predicates.push(parse_quote! { #lt: #bounds }); + } + } + P::Const(_) => {} + } + } + } +} diff --git a/juniper_codegen/src/derive_enum.rs b/juniper_codegen/src/derive_enum.rs index 919d55ec..bf6b3e7a 100644 --- a/juniper_codegen/src/derive_enum.rs +++ b/juniper_codegen/src/derive_enum.rs @@ -138,7 +138,7 @@ pub fn impl_enum(ast: syn::DeriveInput, error: GraphQLScope) -> syn::Result no generics possible generics: syn::Generics::default(), - interfaces: None, + interfaces: vec![], include_type_generics: true, generic_scalar: true, no_async: attrs.no_async.is_some(), diff --git a/juniper_codegen/src/derive_input_object.rs b/juniper_codegen/src/derive_input_object.rs index ab50cc0a..cc8ed3c0 100644 --- a/juniper_codegen/src/derive_input_object.rs +++ b/juniper_codegen/src/derive_input_object.rs @@ -138,7 +138,7 @@ pub fn impl_input_object(ast: syn::DeriveInput, error: GraphQLScope) -> syn::Res description: attrs.description.map(SpanContainer::into_inner), fields, generics: ast.generics, - interfaces: None, + interfaces: vec![], include_type_generics: true, generic_scalar: true, no_async: attrs.no_async.is_some(), diff --git a/juniper_codegen/src/derive_object.rs b/juniper_codegen/src/derive_object.rs index bbcc0870..de2ac2dc 100644 --- a/juniper_codegen/src/derive_object.rs +++ b/juniper_codegen/src/derive_object.rs @@ -89,12 +89,6 @@ pub fn build_derive_object(ast: syn::DeriveInput, error: GraphQLScope) -> syn::R // Early abort after checking all fields proc_macro_error::abort_if_dirty(); - if !attrs.interfaces.is_empty() { - attrs.interfaces.iter().for_each(|elm| { - error.unsupported_attribute(elm.span(), UnsupportedAttribute::Interface) - }); - } - if let Some(duplicates) = crate::util::duplicate::Duplicate::find_by_key(&fields, |field| field.name.as_str()) { @@ -124,7 +118,11 @@ pub fn build_derive_object(ast: syn::DeriveInput, error: GraphQLScope) -> syn::R description: attrs.description.map(SpanContainer::into_inner), fields, generics: ast.generics, - interfaces: None, + interfaces: attrs + .interfaces + .into_iter() + .map(SpanContainer::into_inner) + .collect(), include_type_generics: true, generic_scalar: true, no_async: attrs.no_async.is_some(), diff --git a/juniper_codegen/src/derive_scalar_value.rs b/juniper_codegen/src/derive_scalar_value.rs index 98129a62..b725cfa1 100644 --- a/juniper_codegen/src/derive_scalar_value.rs +++ b/juniper_codegen/src/derive_scalar_value.rs @@ -1,10 +1,11 @@ use crate::{ + common::parse::ParseBufferExt as _, result::GraphQLScope, util::{self, span_container::SpanContainer}, }; use proc_macro2::TokenStream; use quote::quote; -use syn::{self, spanned::Spanned, Data, Fields, Ident, Variant}; +use syn::{spanned::Spanned, token, Data, Fields, Ident, Variant}; #[derive(Debug, Default)] struct TransparentAttributes { @@ -25,12 +26,12 @@ impl syn::parse::Parse for TransparentAttributes { let ident: syn::Ident = input.parse()?; match ident.to_string().as_str() { "name" => { - input.parse::()?; + input.parse::()?; let val = input.parse::()?; output.name = Some(val.value()); } "description" => { - input.parse::()?; + input.parse::()?; let val = input.parse::()?; output.description = Some(val.value()); } @@ -39,9 +40,7 @@ impl syn::parse::Parse for TransparentAttributes { } _ => return Err(syn::Error::new(ident.span(), "unknown attribute")), } - if input.lookahead1().peek(syn::Token![,]) { - input.parse::()?; - } + input.try_parse::()?; } Ok(output) diff --git a/juniper_codegen/src/graphql_interface/attr.rs b/juniper_codegen/src/graphql_interface/attr.rs new file mode 100644 index 00000000..6bda00bb --- /dev/null +++ b/juniper_codegen/src/graphql_interface/attr.rs @@ -0,0 +1,679 @@ +//! Code generation for `#[graphql_interface]` macro. + +use std::mem; + +use proc_macro2::{Span, TokenStream}; +use quote::{quote, ToTokens as _}; +use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned}; + +use crate::{ + common::{ + parse::{self, TypeExt as _}, + ScalarValueType, + }, + result::GraphQLScope, + util::{path_eq_single, span_container::SpanContainer, to_camel_case}, +}; + +use super::{ + inject_async_trait, ArgumentMeta, Definition, EnumType, Field, FieldArgument, ImplMeta, + Implementer, ImplementerDowncast, MethodArgument, MethodMeta, TraitMeta, TraitObjectType, Type, +}; + +/// [`GraphQLScope`] of errors for `#[graphql_interface]` macro. +const ERR: GraphQLScope = GraphQLScope::InterfaceAttr; + +/// Expands `#[graphql_interface]` macro into generated code. +pub fn expand(attr_args: TokenStream, body: TokenStream) -> syn::Result { + if let Ok(mut ast) = syn::parse2::(body.clone()) { + let trait_attrs = parse::attr::unite(("graphql_interface", &attr_args), &ast.attrs); + ast.attrs = parse::attr::strip("graphql_interface", ast.attrs); + return expand_on_trait(trait_attrs, ast); + } else if let Ok(mut ast) = syn::parse2::(body) { + if ast.trait_.is_some() { + let impl_attrs = parse::attr::unite(("graphql_interface", &attr_args), &ast.attrs); + ast.attrs = parse::attr::strip("graphql_interface", ast.attrs); + return expand_on_impl(impl_attrs, ast); + } + } + + Err(syn::Error::new( + Span::call_site(), + "#[graphql_interface] attribute is applicable to trait definitions and trait \ + implementations only", + )) +} + +/// Expands `#[graphql_interface]` macro placed on trait definition. +pub fn expand_on_trait( + attrs: Vec, + mut ast: syn::ItemTrait, +) -> syn::Result { + let meta = TraitMeta::from_attrs("graphql_interface", &attrs)?; + + let trait_ident = &ast.ident; + let trait_span = ast.span(); + + let name = meta + .name + .clone() + .map(SpanContainer::into_inner) + .unwrap_or_else(|| trait_ident.unraw().to_string()); + if !meta.is_internal && name.starts_with("__") { + ERR.no_double_underscore( + meta.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 mut implementers: Vec<_> = meta + .implementers + .iter() + .map(|ty| Implementer { + ty: ty.as_ref().clone(), + downcast: None, + context_ty: None, + scalar: scalar.clone(), + }) + .collect(); + for (ty, downcast) in &meta.external_downcasts { + match implementers.iter_mut().find(|i| &i.ty == ty) { + Some(impler) => { + impler.downcast = Some(ImplementerDowncast::External { + path: downcast.inner().clone(), + }); + } + None => err_only_implementer_downcast(&downcast.span_joined()), + } + } + + proc_macro_error::abort_if_dirty(); + + let mut fields = vec![]; + for item in &mut ast.items { + if let syn::TraitItem::Method(m) = item { + match TraitMethod::parse(m) { + Some(TraitMethod::Field(f)) => fields.push(f), + Some(TraitMethod::Downcast(d)) => { + match implementers.iter_mut().find(|i| i.ty == d.ty) { + Some(impler) => { + if let Some(external) = &impler.downcast { + err_duplicate_downcast(m, external, &impler.ty); + } else { + impler.downcast = d.downcast; + impler.context_ty = d.context_ty; + } + } + None => err_only_implementer_downcast(&m.sig), + } + } + _ => {} + } + } + } + + proc_macro_error::abort_if_dirty(); + + if fields.is_empty() { + ERR.emit_custom(trait_span, "must have at least one field"); + } + + if !all_fields_different(&fields) { + ERR.emit_custom(trait_span, "must have a different name for each field"); + } + + proc_macro_error::abort_if_dirty(); + + let context = meta + .context + .as_ref() + .map(|c| c.as_ref().clone()) + .or_else(|| { + fields.iter().find_map(|f| { + f.arguments + .iter() + .find_map(MethodArgument::context_ty) + .cloned() + }) + }) + .or_else(|| { + implementers + .iter() + .find_map(|impler| impler.context_ty.as_ref()) + .cloned() + }); + + let is_trait_object = meta.r#dyn.is_some(); + + let is_async_trait = meta.asyncness.is_some() + || ast + .items + .iter() + .find_map(|item| match item { + syn::TraitItem::Method(m) => m.sig.asyncness, + _ => None, + }) + .is_some(); + let has_default_async_methods = ast.items.iter().any(|item| match item { + syn::TraitItem::Method(m) => m.sig.asyncness.and(m.default.as_ref()).is_some(), + _ => false, + }); + + let ty = if is_trait_object { + Type::TraitObject(Box::new(TraitObjectType::new( + &ast, + &meta, + scalar.clone(), + context.clone(), + ))) + } else { + Type::Enum(Box::new(EnumType::new( + &ast, + &meta, + &implementers, + scalar.clone(), + ))) + }; + + let generated_code = Definition { + ty, + + name, + description: meta.description.map(SpanContainer::into_inner), + + context, + scalar: scalar.clone(), + + fields, + implementers, + }; + + // Attach the `juniper::AsDynGraphQLValue` on top of the trait if dynamic dispatch is used. + if is_trait_object { + ast.attrs.push(parse_quote! { + #[allow(unused_qualifications, clippy::type_repetition_in_bounds)] + }); + + let scalar_ty = scalar.generic_ty(); + if !scalar.is_explicit_generic() { + let default_ty = scalar.default_ty(); + ast.generics + .params + .push(parse_quote! { #scalar_ty = #default_ty }); + } + ast.generics + .make_where_clause() + .predicates + .push(parse_quote! { #scalar_ty: ::juniper::ScalarValue }); + ast.supertraits + .push(parse_quote! { ::juniper::AsDynGraphQLValue<#scalar_ty> }); + } + + if is_async_trait { + if has_default_async_methods { + // Hack for object safety. See details: https://docs.rs/async-trait/#dyn-traits + ast.supertraits.push(parse_quote! { Sync }); + } + inject_async_trait( + &mut ast.attrs, + ast.items.iter_mut().filter_map(|i| { + if let syn::TraitItem::Method(m) = i { + Some(&mut m.sig) + } else { + None + } + }), + &ast.generics, + ); + } + + 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)?; + + let is_async_trait = meta.asyncness.is_some() + || ast + .items + .iter() + .find_map(|item| match item { + syn::ImplItem::Method(m) => m.sig.asyncness, + _ => None, + }) + .is_some(); + + let is_trait_object = meta.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); + + ast.attrs.push(parse_quote! { + #[allow(unused_qualifications, clippy::type_repetition_in_bounds)] + }); + + if scalar.is_implicit_generic() { + ast.generics.params.push(parse_quote! { #scalar }); + } + if scalar.is_generic() { + ast.generics + .make_where_clause() + .predicates + .push(parse_quote! { #scalar: ::juniper::ScalarValue + Send + Sync }); + } + + if !scalar.is_explicit_generic() { + let (_, trait_path, _) = ast.trait_.as_mut().unwrap(); + let trait_params = &mut trait_path.segments.last_mut().unwrap().arguments; + if let syn::PathArguments::None = trait_params { + *trait_params = syn::PathArguments::AngleBracketed(parse_quote! { <> }); + } + if let syn::PathArguments::AngleBracketed(a) = trait_params { + a.args.push(parse_quote! { #scalar }); + } + } + } + + if is_async_trait { + inject_async_trait( + &mut ast.attrs, + ast.items.iter_mut().filter_map(|i| { + if let syn::ImplItem::Method(m) = i { + Some(&mut m.sig) + } else { + None + } + }), + &ast.generics, + ); + } + + Ok(quote! { #ast }) +} + +/// Representation of parsed Rust trait method for `#[graphql_interface]` macro code generation. +enum TraitMethod { + /// Method represents a [`Field`] of [GraphQL interface][1]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces + Field(Field), + + /// Method represents a custom downcasting function into the [`Implementer`] of + /// [GraphQL interface][1]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces + Downcast(Box), +} + +impl TraitMethod { + /// Parses this [`TraitMethod`] from the given trait method definition. + /// + /// Returns [`None`] if the trait method marked with `#[graphql_interface(ignore)]` attribute, + /// or parsing fails. + #[must_use] + fn parse(method: &mut syn::TraitItemMethod) -> 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_interface")) + .collect(); + + let meta = MethodMeta::from_attrs("graphql_interface", &method_attrs) + .map_err(|e| proc_macro_error::emit_error!(e)) + .ok()?; + + if meta.ignore.is_some() { + return None; + } + + if meta.downcast.is_some() { + return Some(Self::Downcast(Box::new(Self::parse_downcast(method)?))); + } + + Some(Self::Field(Self::parse_field(method, meta)?)) + } + + /// Parses [`TraitMethod::Downcast`] from the given trait method definition. + /// + /// Returns [`None`] if parsing fails. + #[must_use] + fn parse_downcast(method: &mut syn::TraitItemMethod) -> Option { + let method_ident = &method.sig.ident; + + let ty = parse::downcaster::output_type(&method.sig.output) + .map_err(|span| { + ERR.emit_custom( + span, + "expects trait method return type to be `Option<&ImplementerType>` only", + ) + }) + .ok()?; + let context_ty = parse::downcaster::context_ty(&method.sig) + .map_err(|span| { + ERR.emit_custom( + span, + "expects trait method to accept `&self` only and, optionally, `&Context`", + ) + }) + .ok()?; + if let Some(is_async) = &method.sig.asyncness { + ERR.emit_custom( + is_async.span(), + "async downcast to interface implementer is not supported", + ); + return None; + } + + let downcast = ImplementerDowncast::Method { + name: method_ident.clone(), + with_context: context_ty.is_some(), + }; + + Some(Implementer { + ty, + downcast: Some(downcast), + context_ty, + scalar: ScalarValueType::ImplicitGeneric, + }) + } + + /// Parses [`TraitMethod::Field`] from the given trait method definition. + /// + /// Returns [`None`] if parsing fails. + #[must_use] + fn parse_field(method: &mut syn::TraitItemMethod, meta: MethodMeta) -> Option { + let method_ident = &method.sig.ident; + + let name = meta + .name + .as_ref() + .map(|m| m.as_ref().value()) + .unwrap_or_else(|| to_camel_case(&method_ident.unraw().to_string())); + if name.starts_with("__") { + ERR.no_double_underscore( + meta.name + .as_ref() + .map(SpanContainer::span_ident) + .unwrap_or_else(|| method_ident.span()), + ); + return None; + } + + let arguments = { + if method.sig.inputs.is_empty() { + return err_no_method_receiver(&method.sig.inputs); + } + let mut args_iter = method.sig.inputs.iter_mut(); + match args_iter.next().unwrap() { + 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); + } + } + return err_no_method_receiver(arg); + } + }; + args_iter + .filter_map(|arg| match arg { + syn::FnArg::Receiver(_) => None, + syn::FnArg::Typed(arg) => Self::parse_field_argument(arg), + }) + .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 = meta.description.as_ref().map(|d| d.as_ref().value()); + let deprecated = meta + .deprecated + .as_ref() + .map(|d| d.as_ref().as_ref().map(syn::LitStr::value)); + + Some(Field { + name, + ty, + description, + deprecated, + method: method_ident.clone(), + arguments, + 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_interface")) + .collect(); + + let meta = ArgumentMeta::from_attrs("graphql_interface", &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_interface(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_interface({} = ...)]` is not allowed here", + arg, + ), + ) + .emit(); + + None +} + +/// Emits "invalid trait method receiver" [`syn::Error`] pointing to the given `span`. +#[must_use] +fn err_invalid_method_receiver(span: &S) -> Option { + ERR.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`. +#[must_use] +fn err_no_method_receiver(span: &S) -> Option { + ERR.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`. +fn err_only_implementer_downcast(span: &S) { + ERR.custom( + span.span(), + "downcasting is possible only to interface implementers", + ) + .emit(); +} + +/// Emits "duplicate downcast" [`syn::Error`] for the given `method` and `external` +/// [`ImplementerDowncast`] function. +fn err_duplicate_downcast( + method: &syn::TraitItemMethod, + external: &ImplementerDowncast, + impler_ty: &syn::Type, +) { + let external = match external { + ImplementerDowncast::External { path } => path, + _ => unreachable!(), + }; + + ERR.custom( + method.span(), + format!( + "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_interface(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 new file mode 100644 index 00000000..94b07e84 --- /dev/null +++ b/juniper_codegen/src/graphql_interface/mod.rs @@ -0,0 +1,2316 @@ +//! Code generation for [GraphQL interface][1]. +//! +//! [1]: https://spec.graphql.org/June2018/#sec-Interfaces + +pub mod attr; + +use std::collections::{HashMap, HashSet}; + +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens, TokenStreamExt as _}; +use syn::{ + parse::{Parse, ParseStream}, + parse_quote, + spanned::Spanned as _, + token, +}; + +use crate::{ + common::{ + gen, + parse::{ + attr::{err, OptionExt as _}, + GenericsExt as _, ParseBufferExt as _, + }, + ScalarValueType, + }, + util::{filter_attrs, get_deprecated, get_doc_comment, span_container::SpanContainer}, +}; + +/// Available metadata (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 { + /// Explicitly specified name of [GraphQL interface][1] type. + /// + /// If absent, 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. + /// + /// [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. + /// + /// If absent, 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. + /// + /// 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. + /// + /// [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. + /// + /// [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. + /// + /// If absent, then unit type `()` is assumed as 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. + /// + /// 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 + /// [`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>, + + /// 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. + asyncness: Option>, + + /// 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. + /// + /// 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. + is_internal: bool, +} + +impl Parse for TraitMeta { + fn parse(input: ParseStream) -> syn::Result { + let mut output = 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 + .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::()?; + output + .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::()?; + output + .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 + .replace(SpanContainer::new(ident.span(), Some(scl.span()), scl)) + .none_or_else(|_| err::dup_arg(&ident))? + } + "for" | "implementers" => { + input.parse::()?; + for impler in input.parse_maybe_wrapped_and_punctuated::< + syn::Type, token::Bracket, token::Comma, + >()? { + let impler_span = impler.span(); + output + .implementers + .replace(SpanContainer::new(ident.span(), Some(impler_span), impler)) + .none_or_else(|_| err::dup_arg(impler_span))?; + } + } + "dyn" => { + input.parse::()?; + let alias = input.parse::()?; + output + .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 + .replace(SpanContainer::new(ident.span(), Some(alias.span()), alias)) + .none_or_else(|_| err::dup_arg(&ident))? + } + "async" => { + let span = ident.span(); + output + .asyncness + .replace(SpanContainer::new(span, Some(span), ident)) + .none_or_else(|_| err::dup_arg(span))?; + } + "on" => { + let ty = input.parse::()?; + input.parse::()?; + 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 + .insert(ty, dwncst_spanned) + .none_or_else(|_| err::dup_arg(dwncst_span))? + } + "internal" => { + output.is_internal = true; + } + name => { + return Err(err::unknown_arg(&ident, name)); + } + } + input.try_parse::()?; + } + + Ok(output) + } +} + +impl TraitMeta { + /// Tries to merge two [`TraitMeta`]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), + implementers: try_merge_hashset!(implementers: self, another => span_joined), + r#dyn: try_merge_opt!(r#dyn: self, another), + r#enum: try_merge_opt!(r#enum: self, another), + asyncness: try_merge_opt!(asyncness: self, another), + external_downcasts: try_merge_hashmap!( + external_downcasts: self, another => span_joined + ), + is_internal: self.is_internal || another.is_internal, + }) + } + + /// Parses [`TraitMeta`] 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) + .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() { + return Err(syn::Error::new( + as_dyn.span(), + "`dyn` attribute argument is not composable with `enum` attribute argument", + )); + } + } + + if meta.description.is_none() { + meta.description = get_doc_comment(attrs); + } + + Ok(meta) + } +} + +/// Available metadata (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. + /// + /// 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>, + + /// 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. + /// + /// 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). + /// + /// [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(); + + 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 + .replace(SpanContainer::new(ident.span(), Some(scl.span()), scl)) + .none_or_else(|_| err::dup_arg(&ident))? + } + "dyn" => { + let span = ident.span(); + output + .r#dyn + .replace(SpanContainer::new(span, Some(span), ident)) + .none_or_else(|_| err::dup_arg(span))?; + } + "async" => { + let span = ident.span(); + output + .asyncness + .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 ImplMeta { + /// Tries to merge two [`ImplMeta`]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), + r#dyn: try_merge_opt!(r#dyn: self, another), + asyncness: try_merge_opt!(asyncness: self, another), + }) + } + + /// Parses [`ImplMeta`] 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()) + .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?)) + } +} + +/// Available metadata (arguments) behind `#[graphql_interface]` 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_interface]` 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 +struct Definition { + /// Rust type that this [GraphQL interface][1] is represented with. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces + ty: Type, + + /// Name of this [GraphQL interface][1] in GraphQL schema. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces + name: String, + + /// Description of this [GraphQL interface][1] to put into GraphQL schema. + /// + /// [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`]. + /// + /// [`GraphQLType`]: juniper::GraphQLType + /// [`Context`]: juniper::Context + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces + context: Option, + + /// [`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, + + /// Defined [`Field`]s of this [GraphQL interface][1]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces + fields: Vec, + + /// Defined [`Implementer`]s of this [GraphQL interface][1]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces + 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(), + ) + } + } + + /// Returns generated code implementing [`GraphQLType`] trait for this [GraphQL interface][1]. + /// + /// [`GraphQLType`]: juniper::GraphQLType + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces + #[must_use] + fn impl_graphql_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 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 fields_meta = self.fields.iter().map(Field::method_meta_tokens); + + 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, + { + // 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 implementing [`GraphQLValue`] trait for this [GraphQL interface][1]. + /// + /// [`GraphQLValue`]: juniper::GraphQLValue + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces + #[must_use] + fn impl_graphql_value_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 ty = self.ty.ty_tokens(); + let trait_ty = self.ty.trait_ty(); + + let fields_resolvers = 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, + + /// 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, + + /// Indicator whether this [GraphQL field][2] should be resolved asynchronously. + /// + /// [2]: https://spec.graphql.org/June2018/#sec-Language.Fields + is_async: bool, +} + +impl Field { + /// Returns generated code for the [`GraphQLType::meta`] method, which registers this + /// [`Field`] in [`Registry`]. + /// + /// [`GraphQLType::meta`]: juniper::GraphQLType::meta + /// [`Registry`]: juniper::Registry + #[must_use] + fn method_meta_tokens(&self) -> TokenStream { + let (name, ty) = (&self.name, &self.ty); + + 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 arguments = self + .arguments + .iter() + .filter_map(MethodArgument::method_meta_tokens); + + quote! { + registry.field_convert::<#ty, _, Self::Context>(#name, info) + #( #arguments )* + #description + #deprecated + } + } + + /// Returns generated code for the [`GraphQLValue::resolve_field`] method, which resolves this + /// [`Field`] synchronously. + /// + /// Returns [`None`] if this [`Field::is_async`]. + /// + /// [`GraphQLValue::resolve_field`]: juniper::GraphQLValue::resolve_field + #[must_use] + fn method_resolve_field_tokens(&self, trait_ty: &syn::Type) -> Option { + if self.is_async { + return None; + } + + let (name, ty, method) = (&self.name, &self.ty, &self.method); + + let arguments = self + .arguments + .iter() + .map(MethodArgument::method_resolve_field_tokens); + + let resolving_code = gen::sync_resolving_code(); + + Some(quote! { + #name => { + let res: #ty = ::#method(self #( , #arguments )*); + #resolving_code + } + }) + } + + /// Returns generated code for the [`GraphQLValueAsync::resolve_field_async`] method, which + /// resolves this [`Field`] asynchronously. + /// + /// [`GraphQLValueAsync::resolve_field_async`]: juniper::GraphQLValueAsync::resolve_field_async + #[must_use] + fn method_resolve_field_async_tokens(&self, trait_ty: &syn::Type) -> TokenStream { + let (name, ty, method) = (&self.name, &self.ty, &self.method); + + let arguments = self + .arguments + .iter() + .map(MethodArgument::method_resolve_field_tokens); + + let mut fut = quote! { ::#method(self #( , #arguments )*) }; + 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 + } + } + } +} + +/// 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]. + /// + /// [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. + /// + /// [`Context`]: juniper::Context + with_context: bool, + }, + + /// Downcast is performed via some external function. + External { + /// Path of the external function to be called with. + path: syn::ExprPath, + }, +} + +/// Representation of [GraphQL interface][1] implementer for code generation. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Interfaces +#[derive(Clone, Debug)] +struct Implementer { + /// 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. + downcast: Option, + + /// 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. + /// + /// [`Context`]: juniper::Context + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces + context_ty: Option, + + /// [`ScalarValue`] parametrization of this [`Implementer`]. + /// + /// [`ScalarValue`]: juniper::ScalarValue + scalar: ScalarValueType, +} + +impl Implementer { + /// Returns generated code of downcasting this [`Implementer`] via custom + /// [`ImplementerDowncast`]. + /// + /// Returns [`None`] if there is no custom [`Implementer::downcast`]. + #[must_use] + fn downcast_call_tokens( + &self, + trait_ty: &syn::Type, + ctx: Option, + ) -> Option { + let ctx = ctx.unwrap_or_else(|| parse_quote! { executor.context() }); + let mut ctx_arg = Some(quote! { , ::juniper::FromContext::from(#ctx) }); + + let fn_path = match self.downcast.as_ref()? { + ImplementerDowncast::Method { name, with_context } => { + if !with_context { + ctx_arg = None; + } + quote! { ::#name } + } + ImplementerDowncast::External { path } => { + quote! { #path } + } + }; + + Some(quote! { + #fn_path(self #ctx_arg) + }) + } + + /// 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`]. + /// + /// [`GraphQLValue::concrete_type_name`]: juniper::GraphQLValue::concrete_type_name + #[must_use] + fn method_concrete_type_name_tokens(&self, trait_ty: &syn::Type) -> Option { + self.downcast.as_ref()?; + + let ty = &self.ty; + let scalar = &self.scalar; + + let downcast = self.downcast_call_tokens(trait_ty, Some(parse_quote! { 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. + Some(quote! { + if (#downcast as ::std::option::Option<&#ty>).is_some() { + return <#ty as ::juniper::GraphQLType<#scalar>>::name(info).unwrap().to_string(); + } + }) + } + + /// Returns generated code for the [`GraphQLValue::resolve_into_type`] 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 + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces + #[must_use] + fn method_resolve_into_type_tokens(&self, trait_ty: &syn::Type) -> Option { + self.downcast.as_ref()?; + + let ty = &self.ty; + let scalar = &self.scalar; + + let downcast = self.downcast_call_tokens(trait_ty, None); + + let resolving_code = gen::sync_resolving_code(); + + Some(quote! { + if type_name == <#ty as ::juniper::GraphQLType<#scalar>>::name(info).unwrap() { + let res = #downcast; + return #resolving_code; + } + }) + } + + /// 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`]. + /// + /// [0]: juniper::GraphQLValueAsync::resolve_into_type_async + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces + #[must_use] + fn method_resolve_into_type_async_tokens(&self, trait_ty: &syn::Type) -> Option { + self.downcast.as_ref()?; + + let ty = &self.ty; + let scalar = &self.scalar; + + let downcast = self.downcast_call_tokens(trait_ty, None); + + let resolving_code = gen::async_resolving_code(None); + + Some(quote! { + if type_name == <#ty as ::juniper::GraphQLType<#scalar>>::name(info).unwrap() { + let fut = ::juniper::futures::future::ready(#downcast); + return #resolving_code; + } + }) + } +} + +/// Representation of Rust enum implementing [GraphQL interface][1] type for code generation. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Interfaces +struct EnumType { + /// Name of this [`EnumType`] to generate it with. + ident: syn::Ident, + + /// [`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`]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces + variants: Vec, + + /// 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`]. + /// + /// [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`]. + trait_types: Vec<(syn::Ident, syn::Generics)>, + + /// 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`]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces + trait_methods: Vec, + + /// [`ScalarValue`] parametrization to generate [`GraphQLType`] implementation with for this + /// [`EnumType`]. + /// + /// [`GraphQLType`]: juniper::GraphQLType + /// [`ScalarValue`]: juniper::ScalarValue + scalar: ScalarValueType, +} + +impl EnumType { + /// Constructs new [`EnumType`] out of the given parameters. + #[must_use] + fn new( + r#trait: &syn::ItemTrait, + meta: &TraitMeta, + implers: &[Implementer], + scalar: ScalarValueType, + ) -> Self { + Self { + ident: meta + .r#enum + .as_ref() + .map(SpanContainer::as_ref) + .cloned() + .unwrap_or_else(|| format_ident!("{}Value", r#trait.ident)), + visibility: r#trait.vis.clone(), + variants: implers.iter().map(|impler| impler.ty.clone()).collect(), + trait_ident: r#trait.ident.clone(), + trait_generics: r#trait.generics.clone(), + trait_types: r#trait + .items + .iter() + .filter_map(|i| { + if let syn::TraitItem::Type(ty) = i { + Some((ty.ident.clone(), ty.generics.clone())) + } else { + None + } + }) + .collect(), + trait_consts: r#trait + .items + .iter() + .filter_map(|i| { + if let syn::TraitItem::Const(cnst) = i { + Some((cnst.ident.clone(), cnst.ty.clone())) + } else { + None + } + }) + .collect(), + trait_methods: r#trait + .items + .iter() + .filter_map(|i| { + if let syn::TraitItem::Method(m) = i { + Some(m.sig.clone()) + } else { + None + } + }) + .collect(), + scalar, + } + } + + /// Returns name of a single variant of this [`EnumType`] by the given positional `num` in the + /// enum type definition. + #[must_use] + fn variant_ident(num: usize) -> syn::Ident { + format_ident!("Impl{}", num) + } + + /// 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 [`None`] if this [`EnumType`] is exhaustive. + #[must_use] + fn non_exhaustive_match_arm_tokens(&self) -> Option { + if self.has_phantom_variant() || self.variants.is_empty() { + Some(quote! { _ => unreachable!(), }) + } else { + None + } + } + + /// Returns prepared [`syn::Generics`] for [`GraphQLType`] trait (and similar) implementation + /// for this [`EnumType`]. + /// + /// [`GraphQLType`]: juniper::GraphQLType + #[must_use] + fn impl_generics(&self) -> syn::Generics { + let mut generics = self.trait_generics.clone(); + + let scalar = &self.scalar; + if self.scalar.is_implicit_generic() { + generics.params.push(parse_quote! { #scalar }); + } + if self.scalar.is_generic() { + generics + .make_where_clause() + .predicates + .push(parse_quote! { #scalar: ::juniper::ScalarValue }); + } + + generics + } + + /// 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] + fn trait_ty(&self) -> syn::Type { + let ty = &self.trait_ident; + let (_, generics, _) = self.trait_generics.split_for_impl(); + + parse_quote! { #ty#generics } + } + + /// Returns generated code of the full type signature of this [`EnumType`]. + #[must_use] + fn ty_tokens(&self) -> TokenStream { + let ty = &self.ident; + let (_, generics, _) = self.trait_generics.split_for_impl(); + + quote! { #ty#generics } + } + + /// 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. + #[must_use] + fn type_definition_tokens(&self) -> TokenStream { + let enum_ty = &self.ident; + let generics = &self.trait_generics; + let vis = &self.visibility; + + let doc = format!( + "Type implementing [GraphQL interface][1] represented by `{}` trait.\ + \n\n\ + [1]: https://spec.graphql.org/June2018/#sec-Interfaces", + self.trait_ident, + ); + + let variants = self.variants.iter().enumerate().map(|(n, ty)| { + let variant = Self::variant_ident(n); + + quote! { #variant(#ty), } + }); + + let phantom_variant = if self.has_phantom_variant() { + let ty_params = generics.params.iter().map(|p| { + let ty = match p { + syn::GenericParam::Type(ty) => { + let ident = &ty.ident; + quote! { #ident } + } + syn::GenericParam::Lifetime(lt) => { + let lifetime = <.lifetime; + quote! { &#lifetime () } + } + syn::GenericParam::Const(_) => unimplemented!(), + }; + quote! { + ::std::marker::PhantomData<::std::sync::atomic::AtomicPtr>> + } + }); + + Some(quote! { + #[doc(hidden)] + __Phantom(#( #ty_params ),*), + }) + } else { + None + }; + + quote! { + #[automatically_derived] + #[doc = #doc] + #vis enum #enum_ty#generics { + #( #variants )* + #phantom_variant + } + } + } + + /// 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(); + + self.variants.iter().enumerate().map(move |(n, ty)| { + let variant = Self::variant_ident(n); + + quote! { + #[automatically_derived] + impl#impl_generics From<#ty> for #enum_ty#generics #where_clause { + fn from(v: #ty) -> Self { + Self::#variant(v) + } + } + } + }) + } + + /// 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] + fn impl_trait_tokens(&self) -> TokenStream { + let enum_ty = &self.ident; + + let trait_ident = &self.trait_ident; + let (impl_generics, generics, where_clause) = self.trait_generics.split_for_impl(); + + let var_ty = self.variants.first(); + + let assoc_types = self.trait_types.iter().map(|(ty, ty_gen)| { + quote! { + type #ty#ty_gen = <#var_ty as #trait_ident#generics>::#ty#ty_gen; + } + }); + + let assoc_consts = self.trait_consts.iter().map(|(ident, ty)| { + quote! { + const #ident: #ty = <#var_ty as #trait_ident#generics>::#ident; + } + }); + + let methods = self.trait_methods.iter().map(|sig| { + let method = &sig.ident; + + let mut sig = sig.clone(); + let mut args = vec![]; + for (n, arg) in sig.inputs.iter_mut().enumerate() { + match arg { + syn::FnArg::Receiver(_) => {} + syn::FnArg::Typed(a) => { + if !matches!(&*a.pat, syn::Pat::Ident(_)) { + let ident = format_ident!("__arg{}", n); + a.pat = parse_quote! { #ident }; + } + args.push(a.pat.clone()); + } + } + } + + let and_await = if sig.asyncness.is_some() { + Some(quote! { .await }) + } else { + None + }; + + let match_arms = self.variants.iter().enumerate().map(|(n, ty)| { + let variant = Self::variant_ident(n); + let args = args.clone(); + + quote! { + Self::#variant(v) => + <#ty as #trait_ident#generics>::#method(v #( , #args )* )#and_await, + } + }); + let non_exhaustive_match_arm = self.non_exhaustive_match_arm_tokens(); + + quote! { + #sig { + match self { + #( #match_arms )* + #non_exhaustive_match_arm + } + } + } + }); + + let mut impl_tokens = quote! { + #[automatically_derived] + impl#impl_generics #trait_ident#generics for #enum_ty#generics #where_clause { + #( #assoc_types )* + + #( #assoc_consts )* + + #( #methods )* + } + }; + + if self.trait_methods.iter().any(|sig| sig.asyncness.is_some()) { + let mut ast: syn::ItemImpl = parse_quote! { #impl_tokens }; + inject_async_trait( + &mut ast.attrs, + ast.items.iter_mut().filter_map(|i| { + if let syn::ImplItem::Method(m) = i { + Some(&mut m.sig) + } else { + None + } + }), + &ast.generics, + ); + impl_tokens = quote! { #ast }; + } + + 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`]. + /// + /// [`GraphQLValue::concrete_type_name`]: juniper::GraphQLValue::concrete_type_name + #[must_use] + fn method_concrete_type_name_tokens(&self) -> TokenStream { + let scalar = &self.scalar; + + let match_arms = self.variants.iter().enumerate().map(|(n, ty)| { + let variant = Self::variant_ident(n); + + quote! { + Self::#variant(v) => < + #ty as ::juniper::GraphQLValue<#scalar> + >::concrete_type_name(v, context, info), + } + }); + let non_exhaustive_match_arm = self.non_exhaustive_match_arm_tokens(); + + quote! { + match self { + #( #match_arms )* + #non_exhaustive_match_arm + } + } + } + + /// Returns generated code for the [`GraphQLValue::resolve_into_type`] method, which downcasts + /// this [`EnumType`] into its underlying [`Implementer`] type synchronously. + /// + /// [`GraphQLValue::resolve_into_type`]: juniper::GraphQLValue::resolve_into_type + #[must_use] + fn method_resolve_into_type_tokens(&self) -> TokenStream { + let resolving_code = gen::sync_resolving_code(); + + let match_arms = self.variants.iter().enumerate().map(|(n, _)| { + let variant = Self::variant_ident(n); + + quote! { + Self::#variant(res) => #resolving_code, + } + }); + let non_exhaustive_match_arm = self.non_exhaustive_match_arm_tokens(); + + quote! { + match self { + #( #match_arms )* + #non_exhaustive_match_arm + } + } + } + + /// 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] + fn method_resolve_into_type_async_tokens(&self) -> TokenStream { + let resolving_code = gen::async_resolving_code(None); + + let match_arms = self.variants.iter().enumerate().map(|(n, _)| { + let variant = Self::variant_ident(n); + + quote! { + Self::#variant(v) => { + let fut = ::juniper::futures::future::ready(v); + #resolving_code + } + } + }); + let non_exhaustive_match_arm = self.non_exhaustive_match_arm_tokens(); + + quote! { + match self { + #( #match_arms )* + #non_exhaustive_match_arm + } + } + } +} + +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. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Interfaces +/// [2]: https://doc.rust-lang.org/reference/types/trait-object.html +struct TraitObjectType { + /// Name of this [`TraitObjectType`] to generate it with. + ident: syn::Ident, + + /// [`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`]. + /// + /// [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`]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces + trait_generics: syn::Generics, + + /// [`ScalarValue`] parametrization of this [`TraitObjectType`] to generate it with. + /// + /// [`ScalarValue`]: juniper::ScalarValue + scalar: ScalarValueType, + + /// 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, +} + +impl TraitObjectType { + /// Constructs new [`TraitObjectType`] out of the given parameters. + #[must_use] + fn new( + r#trait: &syn::ItemTrait, + meta: &TraitMeta, + scalar: ScalarValueType, + context: Option, + ) -> Self { + Self { + ident: meta.r#dyn.as_ref().unwrap().as_ref().clone(), + visibility: r#trait.vis.clone(), + trait_ident: r#trait.ident.clone(), + trait_generics: r#trait.generics.clone(), + scalar, + context, + } + } + + /// Returns prepared [`syn::Generics`] for [`GraphQLType`] trait (and similar) implementation + /// for this [`TraitObjectType`]. + /// + /// [`GraphQLType`]: juniper::GraphQLType + #[must_use] + fn impl_generics(&self) -> syn::Generics { + let mut generics = self.trait_generics.clone(); + + generics.params.push(parse_quote! { '__obj }); + + 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 }); + } + + generics + } + + /// 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] + fn trait_ty(&self) -> syn::Type { + let ty = &self.trait_ident; + + let mut generics = self.trait_generics.clone(); + if !self.scalar.is_explicit_generic() { + let scalar = &self.scalar; + generics.params.push(parse_quote! { #scalar }); + } + let (_, generics, _) = generics.split_for_impl(); + + parse_quote! { #ty#generics } + } + + /// Returns generated code of the full type signature of this [`TraitObjectType`]. + #[must_use] + fn ty_tokens(&self) -> TokenStream { + let ty = &self.trait_ident; + + let mut generics = self.trait_generics.clone(); + generics.remove_defaults(); + generics.move_bounds_to_where_clause(); + if !self.scalar.is_explicit_generic() { + let scalar = &self.scalar; + generics.params.push(parse_quote! { #scalar }); + } + let ty_params = &generics.params; + + let context = self.context.clone().unwrap_or_else(|| parse_quote! { () }); + + 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`]. + /// + /// [`GraphQLValue::concrete_type_name`]: juniper::GraphQLValue::concrete_type_name + #[must_use] + fn method_concrete_type_name_tokens(&self) -> TokenStream { + quote! { + self.as_dyn_graphql_value().concrete_type_name(context, info) + } + } + + /// Returns generated code for the [`GraphQLValue::resolve_into_type`] method, which downcasts + /// this [`TraitObjectType`] into its underlying [`Implementer`] type synchronously. + /// + /// [`GraphQLValue::resolve_into_type`]: juniper::GraphQLValue::resolve_into_type + #[must_use] + fn method_resolve_into_type_tokens(&self) -> TokenStream { + let resolving_code = gen::sync_resolving_code(); + + quote! { + let res = self.as_dyn_graphql_value(); + #resolving_code + } + } + + /// 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] + fn method_resolve_into_type_async_tokens(&self) -> TokenStream { + let resolving_code = gen::async_resolving_code(None); + + quote! { + let fut = ::juniper::futures::future::ready(self.as_dyn_graphql_value_async()); + #resolving_code + } + } +} + +impl ToTokens for TraitObjectType { + fn to_tokens(&self, into: &mut TokenStream) { + let dyn_ty = &self.ident; + let vis = &self.visibility; + + let doc = format!( + "Helper alias for the `{}` [trait object][2] implementing [GraphQL interface][1].\ + \n\n\ + [1]: https://spec.graphql.org/June2018/#sec-Interfaces\n\ + [2]: https://doc.rust-lang.org/reference/types/trait-object.html", + self.trait_ident, + ); + + let trait_ident = &self.trait_ident; + + let mut generics = self.trait_generics.clone(); + if !self.scalar.is_explicit_generic() { + let scalar_ty = self.scalar.generic_ty(); + let default_ty = self.scalar.default_ty(); + generics + .params + .push(parse_quote! { #scalar_ty = #default_ty }); + } + + let (mut ty_params_left, mut ty_params_right) = (None, None); + if !generics.params.is_empty() { + // We should preserve defaults for left side. + generics.move_bounds_to_where_clause(); + let params = &generics.params; + ty_params_left = Some(quote! { , #params }); + + generics.remove_defaults(); + let params = &generics.params; + ty_params_right = Some(quote! { #params, }); + }; + + let context = self.context.clone().unwrap_or_else(|| parse_quote! { () }); + + let dyn_alias = quote! { + #[automatically_derived] + #[doc = #doc] + #vis type #dyn_ty<'a #ty_params_left> = + dyn #trait_ident<#ty_params_right Context = #context, TypeInfo = ()> + + 'a + Send + Sync; + }; + + into.append_all(&[dyn_alias]); + } +} + +/// Representation of possible Rust types implementing [GraphQL interface][1] type for code +/// generation. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Interfaces +enum Type { + /// [GraphQL interface][1] type implementation as Rust enum. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces + Enum(Box), + + /// [GraphQL interface][1] type implementation as Rust [trait object][2]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces + /// [2]: https://doc.rust-lang.org/reference/types/trait-object.html + TraitObject(Box), +} + +impl Type { + /// Returns prepared [`syn::Generics`] for [`GraphQLType`] trait (and similar) implementation + /// for this [`Type`]. + /// + /// [`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(), + } + } + + /// 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] + fn trait_ty(&self) -> syn::Type { + match self { + Self::Enum(e) => e.trait_ty(), + Self::TraitObject(o) => o.trait_ty(), + } + } + + /// Returns generated code of the full type signature of this [`Type`]. + #[must_use] + fn ty_tokens(&self) -> TokenStream { + match self { + Self::Enum(e) => e.ty_tokens(), + Self::TraitObject(o) => o.ty_tokens(), + } + } + + /// Returns generated code for the [`GraphQLValue::concrete_type_name`] method, which returns + /// name of the underlying [`Implementer`] GraphQL type contained in this [`Type`]. + /// + /// [`GraphQLValue::concrete_type_name`]: juniper::GraphQLValue::concrete_type_name + #[must_use] + fn method_concrete_type_name_tokens(&self) -> TokenStream { + match self { + Self::Enum(e) => e.method_concrete_type_name_tokens(), + Self::TraitObject(o) => o.method_concrete_type_name_tokens(), + } + } + + /// Returns generated code for the [`GraphQLValue::resolve_into_type`] method, which downcasts + /// this [`Type`] into its underlying [`Implementer`] type synchronously. + /// + /// [`GraphQLValue::resolve_into_type`]: juniper::GraphQLValue::resolve_into_type + #[must_use] + fn method_resolve_into_type_tokens(&self) -> TokenStream { + match self { + Self::Enum(e) => e.method_resolve_into_type_tokens(), + Self::TraitObject(o) => o.method_resolve_into_type_tokens(), + } + } + + /// 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 { + match self { + Self::Enum(e) => e.method_resolve_into_type_async_tokens(), + Self::TraitObject(o) => o.method_resolve_into_type_async_tokens(), + } + } +} + +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. +fn inject_async_trait<'m, M>(attrs: &mut Vec, methods: M, generics: &syn::Generics) +where + M: IntoIterator, +{ + attrs.push(parse_quote! { #[::juniper::async_trait] }); + + for method in methods.into_iter() { + if method.asyncness.is_some() { + let where_clause = &mut method.generics.make_where_clause().predicates; + for p in &generics.params { + let ty_param = match p { + syn::GenericParam::Type(t) => { + let ty_param = &t.ident; + quote! { #ty_param } + } + syn::GenericParam::Lifetime(l) => { + let ty_param = &l.lifetime; + quote! { #ty_param } + } + syn::GenericParam::Const(_) => continue, + }; + where_clause.push(parse_quote! { #ty_param: 'async_trait }); + } + } + } +} diff --git a/juniper_codegen/src/graphql_union/attr.rs b/juniper_codegen/src/graphql_union/attr.rs index 1cdea3e3..c7b29ed2 100644 --- a/juniper_codegen/src/graphql_union/attr.rs +++ b/juniper_codegen/src/graphql_union/attr.rs @@ -1,14 +1,15 @@ //! Code generation for `#[graphql_union]` macro. -use std::{mem, ops::Deref as _}; +use std::mem; use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens as _}; use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned as _}; use crate::{ + common::parse, result::GraphQLScope, - util::{path_eq_single, span_container::SpanContainer, unparenthesize}, + util::{path_eq_single, span_container::SpanContainer}, }; use super::{ @@ -27,17 +28,8 @@ pub fn expand(attr_args: TokenStream, body: TokenStream) -> syn::Result syn::Result Result { - let ret_ty = match &sig.output { - syn::ReturnType::Type(_, ty) => ty.deref(), - _ => return Err(sig.span()), - }; - - let path = match unparenthesize(ret_ty) { - syn::Type::Path(syn::TypePath { qself: None, path }) => path, - _ => return Err(ret_ty.span()), - }; - - let (ident, args) = match path.segments.last() { - Some(syn::PathSegment { - ident, - arguments: syn::PathArguments::AngleBracketed(generic), - }) => (ident, &generic.args), - _ => return Err(ret_ty.span()), - }; - - if ident.unraw() != "Option" { - return Err(ret_ty.span()); - } - - if args.len() != 1 { - return Err(ret_ty.span()); - } - let var_ty = match args.first() { - Some(syn::GenericArgument::Type(inner_ty)) => match unparenthesize(inner_ty) { - syn::Type::Reference(inner_ty) => { - if inner_ty.mutability.is_some() { - return Err(inner_ty.span()); - } - unparenthesize(inner_ty.elem.deref()).clone() - } - _ => return Err(ret_ty.span()), - }, - _ => return Err(ret_ty.span()), - }; - Ok(var_ty) -} - -/// Parses trait method input arguments and validates them to be acceptable for resolving into -/// [GraphQL union][1] variant type. Returns type of the context used in input arguments, if any. -/// -/// If input arguments are invalid, then returns the [`Span`] to display the corresponding error at. -/// -/// [1]: https://spec.graphql.org/June2018/#sec-Unions -fn parse_trait_method_input_args(sig: &syn::Signature) -> Result, Span> { - match sig.receiver() { - Some(syn::FnArg::Receiver(rcv)) => { - if rcv.reference.is_none() || rcv.mutability.is_some() { - return Err(rcv.span()); - } - } - _ => return Err(sig.span()), - } - - if sig.inputs.len() > 2 { - return Err(sig.inputs.span()); - } - - let second_arg_ty = match sig.inputs.iter().nth(1) { - Some(syn::FnArg::Typed(arg)) => arg.ty.deref(), - None => return Ok(None), - _ => return Err(sig.inputs.span()), - }; - match unparenthesize(second_arg_ty) { - syn::Type::Reference(ref_ty) => { - if ref_ty.mutability.is_some() { - return Err(ref_ty.span()); - } - Ok(Some(ref_ty.elem.deref().clone())) - } - ty => Err(ty.span()), - } -} diff --git a/juniper_codegen/src/graphql_union/derive.rs b/juniper_codegen/src/graphql_union/derive.rs index 785fe0e8..9d5ac7ff 100644 --- a/juniper_codegen/src/graphql_union/derive.rs +++ b/juniper_codegen/src/graphql_union/derive.rs @@ -6,8 +6,7 @@ use quote::{quote, ToTokens}; use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned as _, Data, Fields}; use crate::{ - result::GraphQLScope, - util::{span_container::SpanContainer, unparenthesize}, + common::parse::TypeExt as _, result::GraphQLScope, util::span_container::SpanContainer, }; use super::{ @@ -85,7 +84,6 @@ fn expand_enum(ast: syn::DeriveInput) -> syn::Result { scalar: meta.scalar.map(SpanContainer::into_inner), generics: ast.generics, variants, - span: enum_span, }) } @@ -115,7 +113,7 @@ fn parse_variant_from_enum_variant( let mut iter = fields.unnamed.iter(); let first = iter.next().unwrap(); if iter.next().is_none() { - Ok(unparenthesize(&first.ty).clone()) + Ok(first.ty.unparenthesized().clone()) } else { Err(fields.span()) } @@ -214,6 +212,5 @@ fn expand_struct(ast: syn::DeriveInput) -> syn::Result { scalar: meta.scalar.map(SpanContainer::into_inner), generics: ast.generics, variants, - span: struct_span, }) } diff --git a/juniper_codegen/src/graphql_union/mod.rs b/juniper_codegen/src/graphql_union/mod.rs index 6ed0a3b5..28586e37 100644 --- a/juniper_codegen/src/graphql_union/mod.rs +++ b/juniper_codegen/src/graphql_union/mod.rs @@ -13,60 +13,16 @@ use syn::{ parse::{Parse, ParseStream}, parse_quote, spanned::Spanned as _, + token, }; -use crate::util::{filter_attrs, get_doc_comment, span_container::SpanContainer, OptionExt as _}; - -/// Attempts to merge an [`Option`]ed `$field` of a `$self` struct with the same `$field` of -/// `$another` struct. If both are [`Some`], then throws a duplication error with a [`Span`] related -/// to the `$another` struct (a later one). -/// -/// The type of [`Span`] may be explicitly specified as one of the [`SpanContainer`] methods. -/// By default, [`SpanContainer::span_ident`] is used. -macro_rules! try_merge_opt { - ($field:ident: $self:ident, $another:ident => $span:ident) => {{ - if let Some(v) = $self.$field { - $another - .$field - .replace(v) - .none_or_else(|dup| dup_attr_err(dup.$span()))?; - } - $another.$field - }}; - - ($field:ident: $self:ident, $another:ident) => { - try_merge_opt!($field: $self, $another => span_ident) - }; -} - -/// Attempts to merge a [`HashMap`]ed `$field` of a `$self` struct with the same `$field` of -/// `$another` struct. If some [`HashMap`] entries are duplicated, then throws a duplication error -/// with a [`Span`] related to the `$another` struct (a later one). -/// -/// The type of [`Span`] may be explicitly specified as one of the [`SpanContainer`] methods. -/// By default, [`SpanContainer::span_ident`] is used. -macro_rules! try_merge_hashmap { - ($field:ident: $self:ident, $another:ident => $span:ident) => {{ - if !$self.$field.is_empty() { - for (ty, rslvr) in $self.$field { - $another - .$field - .insert(ty, rslvr) - .none_or_else(|dup| dup_attr_err(dup.$span()))?; - } - } - $another.$field - }}; - - ($field:ident: $self:ident, $another:ident) => { - try_merge_hashmap!($field: $self, $another => span_ident) - }; -} - -/// Creates and returns duplication error pointing to the given `span`. -fn dup_attr_err(span: Span) -> syn::Error { - syn::Error::new(span, "duplicated attribute") -} +use crate::{ + common::parse::{ + attr::{err, OptionExt as _}, + ParseBufferExt as _, + }, + util::{filter_attrs, get_doc_comment, span_container::SpanContainer}, +}; /// Helper alias for the type of [`UnionMeta::external_resolvers`] field. type UnionMetaResolvers = HashMap>; @@ -131,10 +87,10 @@ impl Parse for UnionMeta { let mut output = Self::default(); while !input.is_empty() { - let ident: syn::Ident = input.parse()?; + let ident = input.parse::()?; match ident.to_string().as_str() { "name" => { - input.parse::()?; + input.parse::()?; let name = input.parse::()?; output .name @@ -143,10 +99,10 @@ impl Parse for UnionMeta { Some(name.span()), name.value(), )) - .none_or_else(|_| dup_attr_err(ident.span()))? + .none_or_else(|_| err::dup_arg(&ident))? } "desc" | "description" => { - input.parse::()?; + input.parse::()?; let desc = input.parse::()?; output .description @@ -155,45 +111,43 @@ impl Parse for UnionMeta { Some(desc.span()), desc.value(), )) - .none_or_else(|_| dup_attr_err(ident.span()))? + .none_or_else(|_| err::dup_arg(&ident))? } "ctx" | "context" | "Context" => { - input.parse::()?; + input.parse::()?; let ctx = input.parse::()?; output .context .replace(SpanContainer::new(ident.span(), Some(ctx.span()), ctx)) - .none_or_else(|_| dup_attr_err(ident.span()))? + .none_or_else(|_| err::dup_arg(&ident))? } "scalar" | "Scalar" | "ScalarValue" => { - input.parse::()?; + input.parse::()?; let scl = input.parse::()?; output .scalar .replace(SpanContainer::new(ident.span(), Some(scl.span()), scl)) - .none_or_else(|_| dup_attr_err(ident.span()))? + .none_or_else(|_| err::dup_arg(&ident))? } "on" => { let ty = input.parse::()?; - input.parse::()?; + input.parse::()?; 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 .insert(ty, rslvr_spanned) - .none_or_else(|_| dup_attr_err(rslvr_span))? + .none_or_else(|_| err::dup_arg(rslvr_span))? } "internal" => { output.is_internal = true; } - _ => { - return Err(syn::Error::new(ident.span(), "unknown attribute")); + name => { + return Err(err::unknown_arg(&ident, name)); } } - if input.lookahead1().peek(syn::Token![,]) { - input.parse::()?; - } + input.try_parse::()?; } Ok(output) @@ -201,19 +155,22 @@ impl Parse for UnionMeta { } impl UnionMeta { - /// Tries to merge two [`UnionMeta`]s into single one, reporting about duplicates, if any. + /// Tries to merge two [`UnionMeta`]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), - external_resolvers: try_merge_hashmap!(external_resolvers: self, another => span_joined), + external_resolvers: try_merge_hashmap!( + external_resolvers: self, another => span_joined + ), is_internal: self.is_internal || another.is_internal, }) } - /// Parses [`UnionMeta`] from the given multiple `name`d attributes placed on type definition. + /// 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 { let mut meta = filter_attrs(name, attrs) .map(|attr| attr.parse_args()) @@ -254,27 +211,25 @@ impl Parse for UnionVariantMeta { let mut output = Self::default(); while !input.is_empty() { - let ident: syn::Ident = input.parse()?; + let ident = input.parse::()?; match ident.to_string().as_str() { "ignore" | "skip" => output .ignore .replace(SpanContainer::new(ident.span(), None, ident.clone())) - .none_or_else(|_| dup_attr_err(ident.span()))?, + .none_or_else(|_| err::dup_arg(&ident))?, "with" => { - input.parse::()?; + input.parse::()?; let rslvr = input.parse::()?; output .external_resolver .replace(SpanContainer::new(ident.span(), Some(rslvr.span()), rslvr)) - .none_or_else(|_| dup_attr_err(ident.span()))? + .none_or_else(|_| err::dup_arg(&ident))? } - _ => { - return Err(syn::Error::new(ident.span(), "unknown attribute")); + name => { + return Err(err::unknown_arg(&ident, name)); } } - if input.lookahead1().peek(syn::Token![,]) { - input.parse::()?; - } + input.try_parse::()?; } Ok(output) @@ -282,7 +237,7 @@ impl Parse for UnionVariantMeta { } impl UnionVariantMeta { - /// Tries to merge two [`UnionVariantMeta`]s into single one, reporting about duplicates, if + /// Tries to merge two [`UnionVariantMeta`]s into a single one, reporting about duplicates, if /// any. fn try_merge(self, mut another: Self) -> syn::Result { Ok(Self { @@ -291,7 +246,7 @@ impl UnionVariantMeta { }) } - /// Parses [`UnionVariantMeta`] from the given multiple `name`d attributes placed on + /// 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 { filter_attrs(name, attrs) @@ -356,6 +311,8 @@ struct UnionDefinition { pub ty: syn::Type, /// 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, /// Indicator whether code should be generated for a trait object, rather than for a regular @@ -390,11 +347,6 @@ struct UnionDefinition { /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub variants: Vec, - - /// [`Span`] that points to the Rust source code which defines this [GraphQL union][1]. - /// - /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub span: Span, } impl ToTokens for UnionDefinition { @@ -432,7 +384,7 @@ impl ToTokens for UnionDefinition { let var_check = &var.resolver_check; quote! { if #var_check { - return <#var_ty as ::juniper::GraphQLType<#scalar>>::name(&()) + return <#var_ty as ::juniper::GraphQLType<#scalar>>::name(info) .unwrap().to_string(); } } @@ -442,15 +394,15 @@ impl ToTokens for UnionDefinition { 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(&())) }; + 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() + executor.context(), ) .and_then(|res| match res { - Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(&(), &r), + Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(info, &r), None => Ok(::juniper::Value::null()), }); } @@ -464,19 +416,19 @@ impl ToTokens for UnionDefinition { let var_ty = &var.ty; let get_name = quote! { - (<#var_ty as ::juniper::GraphQLType<#scalar>>::name(&())) + (<#var_ty as ::juniper::GraphQLType<#scalar>>::name(info)) }; quote! { if type_name == #get_name.unwrap() { let res = ::juniper::IntoResolvable::into( { #expr }, - executor.context() + executor.context(), ); return Box::pin(async move { match res? { Some((ctx, r)) => { let subexec = executor.replaced_context(ctx); - subexec.resolve_with_ctx_async(&(), &r).await + subexec.resolve_with_ctx_async(info, &r).await }, None => Ok(::juniper::Value::null()), } @@ -494,8 +446,7 @@ impl ToTokens for UnionDefinition { if self.scalar.is_none() { ext_generics.params.push(parse_quote! { #scalar }); ext_generics - .where_clause - .get_or_insert_with(|| parse_quote! { where }) + .make_where_clause() .predicates .push(parse_quote! { #scalar: ::juniper::ScalarValue }); } @@ -531,10 +482,10 @@ impl ToTokens for UnionDefinition { ) -> ::juniper::meta::MetaType<'r, #scalar> where #scalar: 'r, { - let types = &[ - #( registry.get_type::<&#var_types>(&(())), )* + let types = [ + #( registry.get_type::<#var_types>(info), )* ]; - registry.build_union_type::<#ty_full>(info, types) + registry.build_union_type::<#ty_full>(info, &types) #description .into_meta() } @@ -556,7 +507,7 @@ impl ToTokens for UnionDefinition { fn concrete_type_name( &self, context: &Self::Context, - _: &Self::TypeInfo, + info: &Self::TypeInfo, ) -> String { #( #match_names )* panic!( @@ -568,7 +519,7 @@ impl ToTokens for UnionDefinition { fn resolve_into_type( &self, - _: &Self::TypeInfo, + info: &Self::TypeInfo, type_name: &str, _: Option<&[::juniper::Selection<#scalar>]>, executor: &::juniper::Executor, @@ -590,7 +541,7 @@ impl ToTokens for UnionDefinition { { fn resolve_into_type_async<'b>( &'b self, - _: &'b Self::TypeInfo, + info: &'b Self::TypeInfo, type_name: &str, _: Option<&'b [::juniper::Selection<'b, #scalar>]>, executor: &'b ::juniper::Executor<'b, 'b, Self::Context, #scalar> diff --git a/juniper_codegen/src/impl_object.rs b/juniper_codegen/src/impl_object.rs index fd824700..66a5879e 100644 --- a/juniper_codegen/src/impl_object.rs +++ b/juniper_codegen/src/impl_object.rs @@ -201,18 +201,12 @@ fn create( description: _impl.description, fields, generics: _impl.generics.clone(), - interfaces: if !_impl.attrs.interfaces.is_empty() { - Some( - _impl - .attrs - .interfaces - .into_iter() - .map(SpanContainer::into_inner) - .collect(), - ) - } else { - None - }, + interfaces: _impl + .attrs + .interfaces + .into_iter() + .map(SpanContainer::into_inner) + .collect(), include_type_generics: false, generic_scalar: false, no_async: _impl.attrs.no_async.is_some(), diff --git a/juniper_codegen/src/impl_scalar.rs b/juniper_codegen/src/impl_scalar.rs index 560dea93..74dc6d7e 100644 --- a/juniper_codegen/src/impl_scalar.rs +++ b/juniper_codegen/src/impl_scalar.rs @@ -26,12 +26,10 @@ fn get_first_method_arg( inputs: syn::punctuated::Punctuated, ) -> Option { if let Some(fn_arg) = inputs.first() { - match fn_arg { - syn::FnArg::Typed(pat_type) => match &*pat_type.pat { - syn::Pat::Ident(pat_ident) => return Some(pat_ident.ident.clone()), - _ => (), - }, - _ => (), + if let syn::FnArg::Typed(pat_type) = fn_arg { + if let syn::Pat::Ident(pat_ident) = &*pat_type.pat { + return Some(pat_ident.ident.clone()); + } } } @@ -70,19 +68,11 @@ fn get_enum_type(return_type: &Option) -> Option { } }); - if let Some(generic_type_arg) = generic_type_arg { - match generic_type_arg { - syn::GenericArgument::Type(the_type) => match the_type { - syn::Type::Path(type_path) => { - if let Some(path_segment) = - type_path.path.segments.first() - { - return Some(path_segment.clone()); - } - } - _ => (), - }, - _ => (), + if let Some(syn::GenericArgument::Type(syn::Type::Path(type_path))) = + generic_type_arg + { + if let Some(path_segment) = type_path.path.segments.first() { + return Some(path_segment.clone()); } } } @@ -115,13 +105,10 @@ impl syn::parse::Parse for ScalarCodegenInput { let custom_data_type_is_struct: bool = !parse_custom_scalar_value_impl.generics.params.is_empty(); - match *parse_custom_scalar_value_impl.self_ty { - syn::Type::Path(type_path) => { - if let Some(path_segment) = type_path.path.segments.first() { - impl_for_type = Some(path_segment.clone()); - } + if let syn::Type::Path(type_path) = *parse_custom_scalar_value_impl.self_ty { + if let Some(path_segment) = type_path.path.segments.first() { + impl_for_type = Some(path_segment.clone()); } - _ => (), } for impl_item in parse_custom_scalar_value_impl.items { diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index d55018d2..a0d498b2 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -12,6 +12,102 @@ extern crate proc_macro; mod result; mod util; +// NOTICE: Unfortunately this macro MUST be defined here, in the crate's root module, because Rust +// doesn't allow to export `macro_rules!` macros from a `proc-macro` crate type currently, +// and so we cannot move the definition into a sub-module and use the `#[macro_export]` +// attribute. +/// Attempts to merge an [`Option`]ed `$field` of a `$self` struct with the same `$field` of +/// `$another` struct. If both are [`Some`], then throws a duplication error with a [`Span`] related +/// to the `$another` struct (a later one). +/// +/// The type of [`Span`] may be explicitly specified as one of the [`SpanContainer`] methods. +/// By default, [`SpanContainer::span_ident`] is used. +/// +/// [`Span`]: proc_macro2::Span +/// [`SpanContainer`]: crate::util::span_container::SpanContainer +/// [`SpanContainer::span_ident`]: crate::util::span_container::SpanContainer::span_ident +macro_rules! try_merge_opt { + ($field:ident: $self:ident, $another:ident => $span:ident) => {{ + if let Some(v) = $self.$field { + $another + .$field + .replace(v) + .none_or_else(|dup| crate::common::parse::attr::err::dup_arg(&dup.$span()))?; + } + $another.$field + }}; + + ($field:ident: $self:ident, $another:ident) => { + try_merge_opt!($field: $self, $another => span_ident) + }; +} + +// NOTICE: Unfortunately this macro MUST be defined here, in the crate's root module, because Rust +// doesn't allow to export `macro_rules!` macros from a `proc-macro` crate type currently, +// and so we cannot move the definition into a sub-module and use the `#[macro_export]` +// attribute. +/// Attempts to merge a [`HashMap`] `$field` of a `$self` struct with the same `$field` of +/// `$another` struct. If some [`HashMap`] entries are duplicated, then throws a duplication error +/// with a [`Span`] related to the `$another` struct (a later one). +/// +/// The type of [`Span`] may be explicitly specified as one of the [`SpanContainer`] methods. +/// By default, [`SpanContainer::span_ident`] is used. +/// +/// [`HashMap`]: std::collections::HashMap +/// [`Span`]: proc_macro2::Span +/// [`SpanContainer`]: crate::util::span_container::SpanContainer +/// [`SpanContainer::span_ident`]: crate::util::span_container::SpanContainer::span_ident +macro_rules! try_merge_hashmap { + ($field:ident: $self:ident, $another:ident => $span:ident) => {{ + if !$self.$field.is_empty() { + for (ty, rslvr) in $self.$field { + $another + .$field + .insert(ty, rslvr) + .none_or_else(|dup| crate::common::parse::attr::err::dup_arg(&dup.$span()))?; + } + } + $another.$field + }}; + + ($field:ident: $self:ident, $another:ident) => { + try_merge_hashmap!($field: $self, $another => span_ident) + }; +} + +// NOTICE: Unfortunately this macro MUST be defined here, in the crate's root module, because Rust +// doesn't allow to export `macro_rules!` macros from a `proc-macro` crate type currently, +// and so we cannot move the definition into a sub-module and use the `#[macro_export]` +// attribute. +/// Attempts to merge a [`HashSet`] `$field` of a `$self` struct with the same `$field` of +/// `$another` struct. If some [`HashSet`] entries are duplicated, then throws a duplication error +/// with a [`Span`] related to the `$another` struct (a later one). +/// +/// The type of [`Span`] may be explicitly specified as one of the [`SpanContainer`] methods. +/// By default, [`SpanContainer::span_ident`] is used. +/// +/// [`HashSet`]: std::collections::HashSet +/// [`Span`]: proc_macro2::Span +/// [`SpanContainer`]: crate::util::span_container::SpanContainer +/// [`SpanContainer::span_ident`]: crate::util::span_container::SpanContainer::span_ident +macro_rules! try_merge_hashset { + ($field:ident: $self:ident, $another:ident => $span:ident) => {{ + if !$self.$field.is_empty() { + for ty in $self.$field { + $another + .$field + .replace(ty) + .none_or_else(|dup| crate::common::parse::attr::err::dup_arg(&dup.$span()))?; + } + } + $another.$field + }}; + + ($field:ident: $self:ident, $another:ident) => { + try_merge_hashset!($field: $self, $another => span_ident) + }; +} + mod derive_enum; mod derive_input_object; mod derive_object; @@ -19,6 +115,8 @@ mod derive_scalar_value; mod impl_object; mod impl_scalar; +mod common; +mod graphql_interface; mod graphql_union; use proc_macro::TokenStream; @@ -452,6 +550,14 @@ pub fn graphql_subscription(args: TokenStream, input: TokenStream) -> TokenStrea )) } +#[proc_macro_error] +#[proc_macro_attribute] +pub fn graphql_interface(attr: TokenStream, body: TokenStream) -> TokenStream { + self::graphql_interface::attr::expand(attr.into(), body.into()) + .unwrap_or_abort() + .into() +} + /// `#[derive(GraphQLUnion)]` macro for deriving a [GraphQL union][1] implementation for enums and /// structs. /// diff --git a/juniper_codegen/src/result.rs b/juniper_codegen/src/result.rs index 76705143..68c878ff 100644 --- a/juniper_codegen/src/result.rs +++ b/juniper_codegen/src/result.rs @@ -10,10 +10,11 @@ pub const SPEC_URL: &str = "https://spec.graphql.org/June2018/"; #[allow(unused_variables)] pub enum GraphQLScope { + InterfaceAttr, UnionAttr, + UnionDerive, DeriveObject, DeriveInputObject, - UnionDerive, DeriveEnum, DeriveScalar, ImplScalar, @@ -23,9 +24,10 @@ pub enum GraphQLScope { impl GraphQLScope { pub fn spec_section(&self) -> &str { match self { + Self::InterfaceAttr => "#sec-Interfaces", + Self::UnionAttr | Self::UnionDerive => "#sec-Unions", Self::DeriveObject | Self::ImplObject => "#sec-Objects", Self::DeriveInputObject => "#sec-Input-Objects", - Self::UnionAttr | Self::UnionDerive => "#sec-Unions", Self::DeriveEnum => "#sec-Enums", Self::DeriveScalar | Self::ImplScalar => "#sec-Scalars", } @@ -35,9 +37,10 @@ impl GraphQLScope { impl fmt::Display for GraphQLScope { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let name = match self { + Self::InterfaceAttr => "interface", + Self::UnionAttr | Self::UnionDerive => "union", Self::DeriveObject | Self::ImplObject => "object", Self::DeriveInputObject => "input object", - Self::UnionAttr | Self::UnionDerive => "union", Self::DeriveEnum => "enum", Self::DeriveScalar | Self::ImplScalar => "scalar", }; diff --git a/juniper_codegen/src/util/mod.rs b/juniper_codegen/src/util/mod.rs index ac6e5a5e..e597eb26 100644 --- a/juniper_codegen/src/util/mod.rs +++ b/juniper_codegen/src/util/mod.rs @@ -1,23 +1,24 @@ #![allow(clippy::single_match)] pub mod duplicate; -pub mod option_ext; pub mod parse_impl; pub mod span_container; -use std::ops::Deref as _; +use std::collections::HashMap; use proc_macro2::{Span, TokenStream}; use proc_macro_error::abort; use quote::quote; use span_container::SpanContainer; -use std::collections::HashMap; use syn::{ - parse, parse_quote, punctuated::Punctuated, spanned::Spanned, Attribute, Lit, Meta, MetaList, - MetaNameValue, NestedMeta, Token, + parse::{Parse, ParseStream}, + parse_quote, + punctuated::Punctuated, + spanned::Spanned, + token, Attribute, Lit, Meta, MetaList, MetaNameValue, NestedMeta, }; -pub use self::option_ext::OptionExt; +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. @@ -74,15 +75,6 @@ pub fn type_is_identifier_ref(ty: &syn::Type, name: &str) -> bool { } } -/// Retrieves the innermost non-parenthesized [`syn::Type`] from the given one (unwraps nested -/// [`syn::TypeParen`]s asap). -pub fn unparenthesize(ty: &syn::Type) -> &syn::Type { - match ty { - syn::Type::Paren(ty) => unparenthesize(ty.elem.deref()), - _ => ty, - } -} - #[derive(Debug)] pub struct DeprecationAttr { pub reason: Option, @@ -232,11 +224,18 @@ fn get_doc_attr(attrs: &[Attribute]) -> Option> { pub fn to_camel_case(s: &str) -> String { let mut dest = String::new(); - // handle '_' to be more friendly with the - // _var convention for unused variables - let s_iter = if s.starts_with('_') { &s[1..] } else { s } - .split('_') - .enumerate(); + // Handle `_` and `__` to be more friendly with the `_var` convention for unused variables, and + // GraphQL introspection identifiers. + let s_iter = if s.starts_with("__") { + dest.push_str("__"); + &s[2..] + } else if s.starts_with('_') { + &s[1..] + } else { + s + } + .split('_') + .enumerate(); for (i, part) in s_iter { if i > 0 && part.len() == 1 { @@ -307,15 +306,15 @@ pub struct ObjectAttributes { pub is_internal: bool, } -impl syn::parse::Parse for ObjectAttributes { - fn parse(input: syn::parse::ParseStream) -> syn::parse::Result { +impl Parse for ObjectAttributes { + fn parse(input: ParseStream) -> syn::Result { let mut output = Self::default(); while !input.is_empty() { - let ident: syn::Ident = input.parse()?; + let ident = input.parse_any_ident()?; match ident.to_string().as_str() { "name" => { - input.parse::()?; + input.parse::()?; let val = input.parse::()?; output.name = Some(SpanContainer::new( ident.span(), @@ -324,7 +323,7 @@ impl syn::parse::Parse for ObjectAttributes { )); } "description" => { - input.parse::()?; + input.parse::()?; let val = input.parse::()?; output.description = Some(SpanContainer::new( ident.span(), @@ -333,7 +332,7 @@ impl syn::parse::Parse for ObjectAttributes { )); } "context" | "Context" => { - input.parse::()?; + input.parse::()?; // TODO: remove legacy support for string based Context. let ctx = if let Ok(val) = input.parse::() { eprintln!("DEPRECATION WARNING: using a string literal for the Context is deprecated"); @@ -345,19 +344,15 @@ impl syn::parse::Parse for ObjectAttributes { output.context = Some(SpanContainer::new(ident.span(), Some(ctx.span()), ctx)); } "scalar" | "Scalar" => { - input.parse::()?; + input.parse::()?; let val = input.parse::()?; output.scalar = Some(SpanContainer::new(ident.span(), Some(val.span()), val)); } - "interfaces" => { - input.parse::()?; - let content; - syn::bracketed!(content in input); - output.interfaces = - syn::punctuated::Punctuated::::parse_terminated( - &content, - )? - .into_iter() + "impl" | "implements" | "interfaces" => { + input.parse::()?; + output.interfaces = input.parse_maybe_wrapped_and_punctuated::< + syn::Type, token::Bracket, token::Comma, + >()?.into_iter() .map(|interface| { SpanContainer::new(ident.span(), Some(interface.span()), interface) }) @@ -374,9 +369,7 @@ impl syn::parse::Parse for ObjectAttributes { return Err(syn::Error::new(ident.span(), "unknown attribute")); } } - if input.lookahead1().peek(syn::Token![,]) { - input.parse::()?; - } + input.try_parse::()?; } Ok(output) @@ -384,7 +377,7 @@ impl syn::parse::Parse for ObjectAttributes { } impl ObjectAttributes { - pub fn from_attrs(attrs: &[syn::Attribute]) -> syn::parse::Result { + pub fn from_attrs(attrs: &[syn::Attribute]) -> syn::Result { let attr_opt = find_graphql_attr(attrs); if let Some(attr) = attr_opt { // Need to unwrap outer (), which are not present for proc macro attributes, @@ -411,8 +404,8 @@ pub struct FieldAttributeArgument { pub description: Option, } -impl parse::Parse for FieldAttributeArgument { - fn parse(input: parse::ParseStream) -> parse::Result { +impl Parse for FieldAttributeArgument { + fn parse(input: ParseStream) -> syn::Result { let name = input.parse()?; let mut arg = Self { @@ -426,7 +419,7 @@ impl parse::Parse for FieldAttributeArgument { syn::parenthesized!(content in input); while !content.is_empty() { let name = content.parse::()?; - content.parse::()?; + content.parse::()?; match name.to_string().as_str() { "name" => { @@ -443,7 +436,7 @@ impl parse::Parse for FieldAttributeArgument { } // Discard trailing comma. - content.parse::().ok(); + content.parse::().ok(); } Ok(arg) @@ -465,13 +458,13 @@ enum FieldAttribute { Default(SpanContainer>), } -impl parse::Parse for FieldAttribute { - fn parse(input: parse::ParseStream) -> parse::Result { +impl Parse for FieldAttribute { + fn parse(input: ParseStream) -> syn::Result { let ident = input.parse::()?; match ident.to_string().as_str() { "name" => { - input.parse::()?; + input.parse::()?; let lit = input.parse::()?; let raw = lit.value(); if !is_valid_name(&raw) { @@ -485,7 +478,7 @@ impl parse::Parse for FieldAttribute { } } "description" => { - input.parse::()?; + input.parse::()?; let lit = input.parse::()?; Ok(FieldAttribute::Description(SpanContainer::new( ident.span(), @@ -494,8 +487,8 @@ impl parse::Parse for FieldAttribute { ))) } "deprecated" | "deprecation" => { - let reason = if input.peek(Token![=]) { - input.parse::()?; + let reason = if input.peek(token::Eq) { + input.parse::()?; Some(input.parse::()?) } else { None @@ -516,7 +509,7 @@ impl parse::Parse for FieldAttribute { "arguments" => { let arg_content; syn::parenthesized!(arg_content in input); - let args = Punctuated::::parse_terminated( + let args = Punctuated::::parse_terminated( &arg_content, )?; let map = args @@ -526,8 +519,8 @@ impl parse::Parse for FieldAttribute { Ok(FieldAttribute::Arguments(map)) } "default" => { - let default_expr = if input.peek(Token![=]) { - input.parse::()?; + let default_expr = if input.peek(token::Eq) { + input.parse::()?; let lit = input.parse::()?; let default_expr = lit.parse::()?; SpanContainer::new(ident.span(), Some(lit.span()), Some(default_expr)) @@ -555,9 +548,9 @@ pub struct FieldAttributes { pub default: Option>>, } -impl parse::Parse for FieldAttributes { - fn parse(input: syn::parse::ParseStream) -> syn::parse::Result { - let items = Punctuated::::parse_terminated(&input)?; +impl Parse for FieldAttributes { + fn parse(input: ParseStream) -> syn::Result { + let items = Punctuated::::parse_terminated(&input)?; let mut output = Self::default(); @@ -596,7 +589,7 @@ impl FieldAttributes { pub fn from_attrs( attrs: &[syn::Attribute], _mode: FieldAttributeParseMode, - ) -> syn::parse::Result { + ) -> syn::Result { let doc_comment = get_doc_comment(&attrs); let deprecation = get_deprecated(&attrs); @@ -669,7 +662,7 @@ pub struct GraphQLTypeDefiniton { pub description: Option, pub fields: Vec, pub generics: syn::Generics, - pub interfaces: Option>, + pub interfaces: Vec, // Due to syn parsing differences, // when parsing an impl the type generics are included in the type // directly, but in syn::DeriveInput, the type generics are @@ -813,13 +806,17 @@ impl GraphQLTypeDefiniton { .as_ref() .map(|description| quote!( .description(#description) )); - let interfaces = self.interfaces.as_ref().map(|items| { - quote!( + let interfaces = if !self.interfaces.is_empty() { + let interfaces_ty = &self.interfaces; + + Some(quote!( .interfaces(&[ - #( registry.get_type::< #items >(&()) ,)* + #( registry.get_type::<#interfaces_ty>(&()) ,)* ]) - ) - }); + )) + } else { + None + }; // Preserve the original type_generics before modification, // since alteration makes them invalid if self.generic_scalar @@ -928,7 +925,29 @@ impl GraphQLTypeDefiniton { .push(parse_quote!( #scalar: Send + Sync )); where_async.predicates.push(parse_quote!(Self: Sync)); - // FIXME: add where clause for interfaces. + 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 @@ -956,6 +975,8 @@ impl GraphQLTypeDefiniton { } } } + + #as_dyn_value ) }; @@ -1144,13 +1165,17 @@ impl GraphQLTypeDefiniton { .as_ref() .map(|description| quote!( .description(#description) )); - let interfaces = self.interfaces.as_ref().map(|items| { - quote!( + let interfaces = if !self.interfaces.is_empty() { + let interfaces_ty = &self.interfaces; + + Some(quote!( .interfaces(&[ - #( registry.get_type::< #items >(&()) ,)* + #( registry.get_type::<#interfaces_ty>(&()) ,)* ]) - ) - }); + )) + } else { + None + }; // Preserve the original type_generics before modification, // since alteration makes them invalid if self.generic_scalar @@ -1917,6 +1942,7 @@ mod test { fn test_to_camel_case() { assert_eq!(&to_camel_case("test")[..], "test"); assert_eq!(&to_camel_case("_test")[..], "test"); + assert_eq!(&to_camel_case("__test")[..], "__test"); assert_eq!(&to_camel_case("first_second")[..], "firstSecond"); assert_eq!(&to_camel_case("first_")[..], "first"); assert_eq!(&to_camel_case("a_b_c")[..], "aBC"); diff --git a/juniper_codegen/src/util/option_ext.rs b/juniper_codegen/src/util/option_ext.rs deleted file mode 100644 index 3007abd6..00000000 --- a/juniper_codegen/src/util/option_ext.rs +++ /dev/null @@ -1,24 +0,0 @@ -/// Handy extension of [`Option`] methods used in this crate. -pub trait OptionExt { - type Inner; - - /// Transforms the `Option` into a `Result<(), E>`, mapping `None` to `Ok(())` and `Some(v)` - /// to `Err(err(v))`. - fn none_or_else(self, err: F) -> Result<(), E> - where - F: FnOnce(Self::Inner) -> E; -} - -impl OptionExt for Option { - type Inner = T; - - fn none_or_else(self, err: F) -> Result<(), E> - where - F: FnOnce(T) -> E, - { - match self { - Some(v) => Err(err(v)), - None => Ok(()), - } - } -} diff --git a/juniper_hyper/examples/hyper_server.rs b/juniper_hyper/examples/hyper_server.rs index 58836986..91222649 100644 --- a/juniper_hyper/examples/hyper_server.rs +++ b/juniper_hyper/examples/hyper_server.rs @@ -5,7 +5,7 @@ use hyper::{ Body, Method, Response, Server, StatusCode, }; use juniper::{ - tests::fixtures::starwars::{model::Database, schema::Query}, + tests::fixtures::starwars::schema::{Database, Query}, EmptyMutation, EmptySubscription, RootNode, }; diff --git a/juniper_hyper/src/lib.rs b/juniper_hyper/src/lib.rs index 409e8ef7..7ead57fe 100644 --- a/juniper_hyper/src/lib.rs +++ b/juniper_hyper/src/lib.rs @@ -319,7 +319,7 @@ mod tests { }; use juniper::{ http::tests as http_tests, - tests::fixtures::starwars::{model::Database, schema::Query}, + tests::fixtures::starwars::schema::{Database, Query}, EmptyMutation, EmptySubscription, RootNode, }; use reqwest::{self, blocking::Response as ReqwestResponse}; diff --git a/juniper_iron/examples/iron_server.rs b/juniper_iron/examples/iron_server.rs index ee86b0a0..74ce4054 100644 --- a/juniper_iron/examples/iron_server.rs +++ b/juniper_iron/examples/iron_server.rs @@ -8,7 +8,7 @@ use std::env; use iron::prelude::*; use juniper::{ - tests::fixtures::starwars::{model::Database, schema::Query}, + tests::fixtures::starwars::schema::{Database, Query}, EmptyMutation, EmptySubscription, }; use juniper_iron::{GraphQLHandler, GraphiQLHandler}; diff --git a/juniper_iron/src/lib.rs b/juniper_iron/src/lib.rs index 82077d62..0e91c8a0 100644 --- a/juniper_iron/src/lib.rs +++ b/juniper_iron/src/lib.rs @@ -421,7 +421,7 @@ mod tests { use juniper::{ http::tests as http_tests, - tests::fixtures::starwars::{model::Database, schema::Query}, + tests::fixtures::starwars::schema::{Database, Query}, EmptyMutation, EmptySubscription, }; diff --git a/juniper_rocket/examples/rocket_server.rs b/juniper_rocket/examples/rocket_server.rs index 8a95d7e4..0a55adad 100644 --- a/juniper_rocket/examples/rocket_server.rs +++ b/juniper_rocket/examples/rocket_server.rs @@ -3,7 +3,7 @@ use rocket::{response::content, State}; use juniper::{ - tests::fixtures::starwars::{model::Database, schema::Query}, + tests::fixtures::starwars::schema::{Database, Query}, EmptyMutation, EmptySubscription, RootNode, }; diff --git a/juniper_rocket/src/lib.rs b/juniper_rocket/src/lib.rs index 89269543..65c3b4dc 100644 --- a/juniper_rocket/src/lib.rs +++ b/juniper_rocket/src/lib.rs @@ -141,8 +141,7 @@ impl GraphQLResponse { /// # use rocket::response::content; /// # use rocket::State; /// # - /// # use juniper::tests::fixtures::starwars::schema::Query; - /// # use juniper::tests::fixtures::starwars::model::Database; + /// # use juniper::tests::fixtures::starwars::schema::{Database, Query}; /// # use juniper::{EmptyMutation, EmptySubscription, FieldError, RootNode, Value}; /// # /// # type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>; @@ -422,7 +421,7 @@ mod fromform_tests { mod tests { use juniper::{ http::tests as http_tests, - tests::fixtures::starwars::{model::Database, schema::Query}, + tests::fixtures::starwars::schema::{Database, Query}, EmptyMutation, EmptySubscription, RootNode, }; use rocket::{ diff --git a/juniper_rocket_async/examples/rocket_server.rs b/juniper_rocket_async/examples/rocket_server.rs index a3489996..def21f33 100644 --- a/juniper_rocket_async/examples/rocket_server.rs +++ b/juniper_rocket_async/examples/rocket_server.rs @@ -1,5 +1,5 @@ use juniper::{ - tests::fixtures::starwars::{model::Database, schema::Query}, + tests::fixtures::starwars::schema::{Database, Query}, EmptyMutation, EmptySubscription, RootNode, }; use rocket::{response::content, State}; diff --git a/juniper_rocket_async/src/lib.rs b/juniper_rocket_async/src/lib.rs index c32b1e70..55b2c448 100644 --- a/juniper_rocket_async/src/lib.rs +++ b/juniper_rocket_async/src/lib.rs @@ -160,8 +160,7 @@ impl GraphQLResponse { /// # use rocket::response::content; /// # use rocket::State; /// # - /// # use juniper::tests::fixtures::starwars::schema::Query; - /// # use juniper::tests::fixtures::starwars::model::Database; + /// # use juniper::tests::fixtures::starwars::schema::{Database, Query}; /// # use juniper::{EmptyMutation, EmptySubscription, FieldError, RootNode, Value}; /// # /// # type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>; @@ -456,7 +455,7 @@ mod tests { use juniper::{ http::tests as http_tests, - tests::fixtures::starwars::{model::Database, schema::Query}, + tests::fixtures::starwars::schema::{Database, Query}, EmptyMutation, EmptySubscription, RootNode, }; use rocket::{ diff --git a/juniper_warp/examples/warp_server.rs b/juniper_warp/examples/warp_server.rs index 9eb14e20..d0f1312c 100644 --- a/juniper_warp/examples/warp_server.rs +++ b/juniper_warp/examples/warp_server.rs @@ -3,7 +3,7 @@ use std::env; use juniper::{ - tests::fixtures::starwars::{model::Database, schema::Query}, + tests::fixtures::starwars::schema::{Database, Query}, EmptyMutation, EmptySubscription, RootNode, }; use warp::{http::Response, Filter}; diff --git a/juniper_warp/src/lib.rs b/juniper_warp/src/lib.rs index 495e7249..3d270143 100644 --- a/juniper_warp/src/lib.rs +++ b/juniper_warp/src/lib.rs @@ -602,7 +602,7 @@ mod tests { #[tokio::test] async fn graphql_handler_works_json_post() { use juniper::{ - tests::fixtures::starwars::{model::Database, schema::Query}, + tests::fixtures::starwars::schema::{Database, Query}, EmptyMutation, EmptySubscription, RootNode, }; @@ -641,7 +641,7 @@ mod tests { #[tokio::test] async fn batch_requests_work() { use juniper::{ - tests::fixtures::starwars::{model::Database, schema::Query}, + tests::fixtures::starwars::schema::{Database, Query}, EmptyMutation, EmptySubscription, RootNode, }; @@ -696,7 +696,7 @@ mod tests_http_harness { use super::*; use juniper::{ http::tests::{run_http_test_suite, HttpIntegration, TestResponse}, - tests::fixtures::starwars::{model::Database, schema::Query}, + tests::fixtures::starwars::schema::{Database, Query}, EmptyMutation, EmptySubscription, RootNode, }; use warp::{