diff --git a/docs/book/content/types/unions.md b/docs/book/content/types/unions.md index a2793a62..16775eb5 100644 --- a/docs/book/content/types/unions.md +++ b/docs/book/content/types/unions.md @@ -1,36 +1,268 @@ -# Unions +Unions +====== -From a server's point of view, GraphQL unions are similar to interfaces: the -only exception is that they don't contain fields on their own. +From the server's point of view, [GraphQL unions][1] are similar to interfaces - the only exception is that they don't contain fields on their own. -In Juniper, the `graphql_union!` has identical syntax to the -[interface macro](interfaces.md), but does not support defining -fields. Therefore, the same considerations about using traits, -placeholder types, or enums still apply to unions. For simple -situations, Juniper provides `#[derive(GraphQLUnion)]` for enums. +For implementing [GraphQL unions][1] Juniper provides: +- `#[derive(GraphQLUnion)]` macro for enums and structs. +- `#[graphql_union]` for traits. -If we look at the same examples as in the interfaces chapter, we see the -similarities and the tradeoffs: -## Traits -### Downcasting via accessor methods + +## Enums + +Most of the time, we just need a trivial and straightforward Rust enum to represent a [GraphQL union][1]. ```rust -#[derive(juniper::GraphQLObject)] +# #![allow(dead_code)] +use derive_more::From; +use juniper::{GraphQLObject, GraphQLUnion}; + +#[derive(GraphQLObject)] struct Human { id: String, home_planet: String, } -#[derive(juniper::GraphQLObject)] +#[derive(GraphQLObject)] struct Droid { id: String, primary_function: String, } +#[derive(From, GraphQLUnion)] +enum Character { + Human(Human), + Droid(Droid), +} +# +# fn main() {} +``` + + +### Ignoring enum variants + +In some rare situations we may want to omit exposing an enum variant in the GraphQL schema. + +As an example, let's consider the situation where we need to bind some type parameter `T` for doing interesting type-level stuff in our resolvers. To achieve this we need to have `PhantomData`, but we don't want it exposed in the GraphQL schema. + +> __WARNING__: +> It's the _library user's responsibility_ to ensure that ignored enum variant is _never_ returned from resolvers, otherwise resolving the GraphQL query will __panic at runtime__. + +```rust +# use std::marker::PhantomData; +use derive_more::From; +use juniper::{GraphQLObject, GraphQLUnion}; + +#[derive(GraphQLObject)] +struct Human { + id: String, + home_planet: String, +} + +#[derive(GraphQLObject)] +struct Droid { + id: String, + primary_function: String, +} + +#[derive(From, GraphQLUnion)] +enum Character { + Human(Human), + Droid(Droid), + #[from(ignore)] + #[graphql(ignore)] // or `#[graphql(skip)]`, your choice + _State(PhantomData), +} +# +# fn main() {} +``` + + +### External resolver functions + +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)] +use juniper::{GraphQLObject, GraphQLUnion}; + +#[derive(GraphQLObject)] +#[graphql(Context = CustomContext)] +struct Human { + id: String, + home_planet: String, +} + +#[derive(GraphQLObject)] +#[graphql(Context = CustomContext)] +struct Droid { + id: String, + primary_function: String, +} + +pub struct CustomContext { + droid: Droid, +} +impl juniper::Context for CustomContext {} + +#[derive(GraphQLUnion)] +#[graphql(Context = CustomContext)] +enum Character { + Human(Human), + #[graphql(with = Character::droid_from_context)] + Droid(Droid), +} + +impl Character { + // NOTICE: The function signature must contain `&self` and `&Context`, + // and return `Option<&VariantType>`. + fn droid_from_context<'c>(&self, ctx: &'c CustomContext) -> Option<&'c Droid> { + Some(&ctx.droid) + } +} +# +# fn main() {} +``` + +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)] +use juniper::{GraphQLObject, GraphQLUnion}; + +#[derive(GraphQLObject)] +#[graphql(Context = CustomContext)] +struct Human { + id: String, + home_planet: String, +} + +#[derive(GraphQLObject)] +#[graphql(Context = CustomContext)] +struct Droid { + id: String, + primary_function: String, +} + +#[derive(GraphQLObject)] +#[graphql(Context = CustomContext)] +struct Ewok { + id: String, + is_funny: bool, +} + +pub struct CustomContext { + ewok: Ewok, +} +impl juniper::Context for CustomContext {} + +#[derive(GraphQLUnion)] +#[graphql(Context = CustomContext)] +#[graphql(on Ewok = Character::ewok_from_context)] +enum Character { + Human(Human), + Droid(Droid), + #[graphql(ignore)] // or `#[graphql(skip)]`, your choice + Ewok, +} + +impl Character { + fn ewok_from_context<'c>(&self, ctx: &'c CustomContext) -> Option<&'c Ewok> { + if let Self::Ewok = self { + Some(&ctx.ewok) + } else { + None + } + } +} +# +# fn main() {} +``` + + + + +## Structs + +Using Rust structs as [GraphQL unions][1] is very similar to using enums, with the nuance that specifying an external resolver function is the only way to declare a [GraphQL union][1] variant. + +```rust +# use std::collections::HashMap; +use juniper::{GraphQLObject, GraphQLUnion}; + +#[derive(GraphQLObject)] +#[graphql(Context = Database)] +struct Human { + id: String, + home_planet: String, +} + +#[derive(GraphQLObject)] +#[graphql(Context = Database)] +struct Droid { + id: String, + primary_function: String, +} + +struct Database { + humans: HashMap, + droids: HashMap, +} +impl juniper::Context for Database {} + +#[derive(GraphQLUnion)] +#[graphql( + Context = Database, + on Human = Character::get_human, + on Droid = Character::get_droid, +)] +struct Character { + id: String, +} + +impl Character { + fn get_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human>{ + ctx.humans.get(&self.id) + } + + fn get_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid>{ + ctx.droids.get(&self.id) + } +} +# +# fn main() {} +``` + + + + +## Traits + +To use a Rust trait definition as a [GraphQL union][1] you need to use the `#[graphql_union]` macro. [Rust doesn't allow derive macros on traits](https://doc.rust-lang.org/stable/reference/procedural-macros.html#derive-macros), so using `#[derive(GraphQLUnion)]` on traits doesn't work. + +> __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](https://doc.rust-lang.org/stable/reference/types/trait-object.html) to specify a [GraphQL union][1] behind it. + +```rust +use juniper::{graphql_union, GraphQLObject}; + +#[derive(GraphQLObject)] +struct Human { + id: String, + home_planet: String, +} + +#[derive(GraphQLObject)] +struct Droid { + id: String, + primary_function: String, +} + +#[graphql_union] trait Character { - // Downcast methods, each concrete class will need to implement one of these + // NOTICE: The method signature must contain `&self` and return `Option<&VariantType>`. fn as_human(&self) -> Option<&Human> { None } fn as_droid(&self) -> Option<&Droid> { None } } @@ -42,34 +274,28 @@ impl Character for Human { impl Character for Droid { fn as_droid(&self) -> Option<&Droid> { Some(&self) } } - -#[juniper::graphql_union] -impl<'a> GraphQLUnion for &'a dyn Character { - fn resolve(&self) { - match self { - Human => self.as_human(), - Droid => self.as_droid(), - } - } -} - +# # fn main() {} ``` -### Using an extra database lookup -FIXME: This example does not compile at the moment +### Custom context + +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)] # use std::collections::HashMap; -#[derive(juniper::GraphQLObject)] +use juniper::{graphql_union, GraphQLObject}; + +#[derive(GraphQLObject)] #[graphql(Context = Database)] struct Human { id: String, home_planet: String, } -#[derive(juniper::GraphQLObject)] +#[derive(GraphQLObject)] #[graphql(Context = Database)] struct Droid { id: String, @@ -80,10 +306,107 @@ struct Database { humans: HashMap, droids: HashMap, } - impl juniper::Context for Database {} +#[graphql_union(Context = Database)] trait Character { + // NOTICE: The method signature may optionally contain `&Context`. + fn as_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> { None } + fn as_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid> { None } +} + +impl Character for Human { + fn as_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> { + ctx.humans.get(&self.id) + } +} + +impl Character for Droid { + fn as_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid> { + ctx.droids.get(&self.id) + } +} +# +# fn main() {} +``` + + +### Ignoring trait methods + +As with enums, we may want to omit some trait methods to be assumed as [GraphQL union][1] variants and ignore them. + +```rust +use juniper::{graphql_union, GraphQLObject}; + +#[derive(GraphQLObject)] +struct Human { + id: String, + home_planet: String, +} + +#[derive(GraphQLObject)] +struct Droid { + id: String, + primary_function: String, +} + +#[graphql_union] +trait Character { + fn as_human(&self) -> Option<&Human> { None } + fn as_droid(&self) -> Option<&Droid> { None } + #[graphql_union(ignore)] // or `#[graphql_union(skip)]`, your choice + fn id(&self) -> &str; +} + +impl Character for Human { + fn as_human(&self) -> Option<&Human> { Some(&self) } + fn id(&self) -> &str { self.id.as_str() } +} + +impl Character for Droid { + fn as_droid(&self) -> Option<&Droid> { Some(&self) } + fn id(&self) -> &str { self.id.as_str() } +} +# +# fn main() {} +``` + + +### External resolver functions + +Similarly to enums and structs, it's not mandatory to use trait methods as [GraphQL union][1] variant resolvers. Instead, custom functions may be specified: + +```rust +# use std::collections::HashMap; +use juniper::{graphql_union, GraphQLObject}; + +#[derive(GraphQLObject)] +#[graphql(Context = Database)] +struct Human { + id: String, + home_planet: String, +} + +#[derive(GraphQLObject)] +#[graphql(Context = Database)] +struct Droid { + id: String, + primary_function: String, +} + +struct Database { + humans: HashMap, + droids: HashMap, +} +impl juniper::Context for Database {} + +#[graphql_union(Context = Database)] +#[graphql_union( + on Human = DynCharacter::get_human, + on Droid = get_droid, +)] +trait Character { + #[graphql_union(ignore)] // or `#[graphql_union(skip)]`, your choice fn id(&self) -> &str; } @@ -95,125 +418,61 @@ impl Character for Droid { fn id(&self) -> &str { self.id.as_str() } } +// The trait object is always `Send` and `Sync`. +type DynCharacter<'a> = dyn Character + Send + Sync + 'a; -#[juniper::graphql_union( - Context = Database -)] -impl<'a> GraphQLUnion for &'a dyn Character { - fn resolve(&self, context: &Database) { - match self { - Human => context.humans.get(self.id()), - Droid => context.droids.get(self.id()), - } +impl<'a> DynCharacter<'a> { + fn get_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> { + ctx.humans.get(self.id()) } } +// External resolver function doesn't have to be a method of a type. +// It's only a matter of the function signature to match the requirements. +fn get_droid<'db>(ch: &DynCharacter<'_>, ctx: &'db Database) -> Option<&'db Droid> { + ctx.droids.get(ch.id()) +} +# # fn main() {} ``` -## Placeholder objects + + + +## `ScalarValue` considerations + +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 -# use std::collections::HashMap; -#[derive(juniper::GraphQLObject)] -#[graphql(Context = Database)] +# #![allow(dead_code)] +use juniper::{DefaultScalarValue, GraphQLObject, GraphQLUnion}; + +#[derive(GraphQLObject)] +#[graphql(Scalar = DefaultScalarValue)] struct Human { id: String, home_planet: String, } -#[derive(juniper::GraphQLObject)] -#[graphql(Context = Database)] +#[derive(GraphQLObject)] struct Droid { id: String, primary_function: String, } -struct Database { - humans: HashMap, - droids: HashMap, -} - -impl juniper::Context for Database {} - -struct Character { - id: String, -} - -#[juniper::graphql_union( - Context = Database, -)] -impl GraphQLUnion for Character { - fn resolve(&self, context: &Database) { - match self { - Human => { context.humans.get(&self.id) }, - Droid => { context.droids.get(&self.id) }, - } - } -} - -# fn main() {} -``` - -## Enums (Impl) - -```rust -#[derive(juniper::GraphQLObject)] -struct Human { - id: String, - home_planet: String, -} - -#[derive(juniper::GraphQLObject)] -struct Droid { - id: String, - primary_function: String, -} - -# #[allow(dead_code)] +#[derive(GraphQLUnion)] +#[graphql(Scalar = DefaultScalarValue)] // removing this line will fail compilation enum Character { Human(Human), Droid(Droid), } - -#[juniper::graphql_union] -impl Character { - fn resolve(&self) { - match self { - Human => { match *self { Character::Human(ref h) => Some(h), _ => None } }, - Droid => { match *self { Character::Droid(ref d) => Some(d), _ => None } }, - } - } -} - +# # fn main() {} ``` -## Enums (Derive) -This example is similar to `Enums (Impl)`. To successfully use the -derive macro, ensure that each variant of the enum has a different -type. Since each variant is different, the device macro provides -`std::convert::Into` converter for each variant. -```rust -#[derive(juniper::GraphQLObject)] -struct Human { - id: String, - home_planet: String, -} -#[derive(juniper::GraphQLObject)] -struct Droid { - id: String, - primary_function: String, -} -#[derive(juniper::GraphQLUnion)] -enum Character { - Human(Human), - Droid(Droid), -} - -# fn main() {} -``` +[1]: https://spec.graphql.org/June2018/#sec-Unions +[2]: https://docs.rs/juniper/latest/juniper/trait.ScalarValue.html diff --git a/docs/book/tests/Cargo.toml b/docs/book/tests/Cargo.toml index d7c4d0bc..120f1a89 100644 --- a/docs/book/tests/Cargo.toml +++ b/docs/book/tests/Cargo.toml @@ -9,6 +9,8 @@ build = "build.rs" juniper = { path = "../../../juniper" } juniper_iron = { path = "../../../juniper_iron" } juniper_subscriptions = { path = "../../../juniper_subscriptions" } + +derive_more = "0.99.7" futures = "0.3" tokio = { version = "0.2", features = ["rt-core", "blocking", "stream", "rt-util"] } iron = "0.5.0" diff --git a/integration_tests/codegen_fail/fail/union/attr_wrong_item.rs b/integration_tests/codegen_fail/fail/union/attr_wrong_item.rs new file mode 100644 index 00000000..76d7b27e --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/attr_wrong_item.rs @@ -0,0 +1,6 @@ +use juniper::graphql_union; + +#[graphql_union] +enum Character {} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/attr_wrong_item.stderr b/integration_tests/codegen_fail/fail/union/attr_wrong_item.stderr new file mode 100644 index 00000000..419835d5 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/attr_wrong_item.stderr @@ -0,0 +1,7 @@ +error: #[graphql_union] attribute is applicable to trait definitions only + --> $DIR/attr_wrong_item.rs:3:1 + | +3 | #[graphql_union] + | ^^^^^^^^^^^^^^^^ + | + = 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/derive_enum_field.rs b/integration_tests/codegen_fail/fail/union/derive_enum_field.rs deleted file mode 100644 index 11b94506..00000000 --- a/integration_tests/codegen_fail/fail/union/derive_enum_field.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[derive(juniper::GraphQLEnum)] -pub enum Test { - A, - B, -} - -#[derive(juniper::GraphQLUnion)] -enum Character { - Test(Test), -} - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/derive_enum_field.stderr b/integration_tests/codegen_fail/fail/union/derive_enum_field.stderr deleted file mode 100644 index 58d13adf..00000000 --- a/integration_tests/codegen_fail/fail/union/derive_enum_field.stderr +++ /dev/null @@ -1,8 +0,0 @@ -error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType<__S>` is not satisfied - --> $DIR/derive_enum_field.rs:7:10 - | -7 | #[derive(juniper::GraphQLUnion)] - | ^^^^^^^^^^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType<__S>` is not implemented for `Test` - | - = note: required by `juniper::types::marker::GraphQLObjectType::mark` - = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/union/derive_no_fields.rs b/integration_tests/codegen_fail/fail/union/derive_no_fields.rs deleted file mode 100644 index 4e4cb17d..00000000 --- a/integration_tests/codegen_fail/fail/union/derive_no_fields.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[derive(juniper::GraphQLUnion)] -enum Character {} - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/derive_no_fields.stderr b/integration_tests/codegen_fail/fail/union/derive_no_fields.stderr deleted file mode 100644 index 5c069f6f..00000000 --- a/integration_tests/codegen_fail/fail/union/derive_no_fields.stderr +++ /dev/null @@ -1,7 +0,0 @@ -error: GraphQL union expects at least one field - --> $DIR/derive_no_fields.rs:2:1 - | -2 | enum Character {} - | ^^^^^^^^^^^^^^^^^ - | - = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/codegen_fail/fail/union/derive_same_type.stderr b/integration_tests/codegen_fail/fail/union/derive_same_type.stderr deleted file mode 100644 index 6e62556e..00000000 --- a/integration_tests/codegen_fail/fail/union/derive_same_type.stderr +++ /dev/null @@ -1,10 +0,0 @@ -error[E0119]: conflicting implementations of trait `std::convert::From` for type `Character`: - --> $DIR/derive_same_type.rs:1:10 - | -1 | #[derive(juniper::GraphQLUnion)] - | ^^^^^^^^^^^^^^^^^^^^^ - | | - | first implementation here - | conflicting implementation for `Character` - | - = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/union/derive_wrong_item.rs b/integration_tests/codegen_fail/fail/union/derive_wrong_item.rs new file mode 100644 index 00000000..174cd2ad --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/derive_wrong_item.rs @@ -0,0 +1,6 @@ +use juniper::GraphQLUnion; + +#[derive(GraphQLUnion)] +union Character { id: i32 } + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/derive_wrong_item.stderr b/integration_tests/codegen_fail/fail/union/derive_wrong_item.stderr new file mode 100644 index 00000000..fdeb15b4 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/derive_wrong_item.stderr @@ -0,0 +1,5 @@ +error: GraphQL union can only be derived for enums and structs + --> $DIR/derive_wrong_item.rs:4:1 + | +4 | union Character { id: i32 } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/integration_tests/codegen_fail/fail/union/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.rs b/integration_tests/codegen_fail/fail/union/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.rs new file mode 100644 index 00000000..2da4466f --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.rs @@ -0,0 +1,15 @@ +use juniper::{GraphQLObject, GraphQLUnion}; + +#[derive(GraphQLUnion)] +#[graphql(on Human = resolve_fn1)] +enum Character { + #[graphql(with = resolve_fn2)] + A(Human), +} + +#[derive(GraphQLObject)] +pub struct Human { + id: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.stderr b/integration_tests/codegen_fail/fail/union/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.stderr new file mode 100644 index 00000000..12957f13 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.stderr @@ -0,0 +1,7 @@ +error: GraphQL union variant `Human` already has external resolver function `resolve_fn1` declared on the enum + --> $DIR/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.rs:6:15 + | +6 | #[graphql(with = resolve_fn2)] + | ^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/codegen_fail/fail/union/enum_name_double_underscored.rs b/integration_tests/codegen_fail/fail/union/enum_name_double_underscored.rs new file mode 100644 index 00000000..2d0fa690 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_name_double_underscored.rs @@ -0,0 +1,13 @@ +use juniper::{GraphQLObject, GraphQLUnion}; + +#[derive(GraphQLUnion)] +enum __Character { + A(Human), +} + +#[derive(GraphQLObject)] +pub struct Human { + id: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/enum_name_double_underscored.stderr b/integration_tests/codegen_fail/fail/union/enum_name_double_underscored.stderr new file mode 100644 index 00000000..df5db23f --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_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/enum_name_double_underscored.rs:4:6 + | +4 | enum __Character { + | ^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Schema diff --git a/integration_tests/codegen_fail/fail/union/enum_no_fields.rs b/integration_tests/codegen_fail/fail/union/enum_no_fields.rs new file mode 100644 index 00000000..66d35f02 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_no_fields.rs @@ -0,0 +1,6 @@ +use juniper::GraphQLUnion; + +#[derive(GraphQLUnion)] +enum Character {} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/enum_no_fields.stderr b/integration_tests/codegen_fail/fail/union/enum_no_fields.stderr new file mode 100644 index 00000000..85ea2585 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_no_fields.stderr @@ -0,0 +1,7 @@ +error: GraphQL union expects at least one union variant + --> $DIR/enum_no_fields.rs:4:1 + | +4 | enum Character {} + | ^^^^^^^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/codegen_fail/fail/union/enum_non_object_variant.rs b/integration_tests/codegen_fail/fail/union/enum_non_object_variant.rs new file mode 100644 index 00000000..1019baf1 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_non_object_variant.rs @@ -0,0 +1,14 @@ +use juniper::{GraphQLEnum, GraphQLUnion}; + +#[derive(GraphQLEnum)] +pub enum Test { + A, + B, +} + +#[derive(GraphQLUnion)] +enum Character { + Test(Test), +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/enum_non_object_variant.stderr b/integration_tests/codegen_fail/fail/union/enum_non_object_variant.stderr new file mode 100644 index 00000000..1da87bdd --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_non_object_variant.stderr @@ -0,0 +1,17 @@ +error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType` is not satisfied + --> $DIR/enum_non_object_variant.rs:9:10 + | +9 | #[derive(GraphQLUnion)] + | ^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType` is not implemented for `Test` + | + = note: required by `juniper::types::marker::GraphQLObjectType::mark` + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType<__S>` is not satisfied + --> $DIR/enum_non_object_variant.rs:9:10 + | +9 | #[derive(GraphQLUnion)] + | ^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType<__S>` is not implemented for `Test` + | + = note: required by `juniper::types::marker::GraphQLObjectType::mark` + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/union/enum_same_type_pretty.rs b/integration_tests/codegen_fail/fail/union/enum_same_type_pretty.rs new file mode 100644 index 00000000..cbc1a12e --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_same_type_pretty.rs @@ -0,0 +1,9 @@ +use juniper::GraphQLUnion; + +#[derive(GraphQLUnion)] +enum Character { + A(u8), + B(u8), +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/enum_same_type_pretty.stderr b/integration_tests/codegen_fail/fail/union/enum_same_type_pretty.stderr new file mode 100644 index 00000000..65cac9c3 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_same_type_pretty.stderr @@ -0,0 +1,10 @@ +error: GraphQL union must have a different type for each union variant + --> $DIR/enum_same_type_pretty.rs:4:1 + | +4 | / enum Character { +5 | | A(u8), +6 | | B(u8), +7 | | } + | |_^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/codegen_fail/fail/union/derive_same_type.rs b/integration_tests/codegen_fail/fail/union/enum_same_type_ugly.rs similarity index 59% rename from integration_tests/codegen_fail/fail/union/derive_same_type.rs rename to integration_tests/codegen_fail/fail/union/enum_same_type_ugly.rs index e267a601..28cf2af8 100644 --- a/integration_tests/codegen_fail/fail/union/derive_same_type.rs +++ b/integration_tests/codegen_fail/fail/union/enum_same_type_ugly.rs @@ -1,4 +1,6 @@ -#[derive(juniper::GraphQLUnion)] +use juniper::GraphQLUnion; + +#[derive(GraphQLUnion)] enum Character { A(std::string::String), B(String), 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 new file mode 100644 index 00000000..236daf7f --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_same_type_ugly.stderr @@ -0,0 +1,10 @@ +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)] + | ^^^^^^^^^^^^ + | | + | first implementation here + | conflicting implementation for `std::string::String` + | + = 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/union/enum_wrong_variant_field.rs b/integration_tests/codegen_fail/fail/union/enum_wrong_variant_field.rs new file mode 100644 index 00000000..84492eb8 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_wrong_variant_field.rs @@ -0,0 +1,18 @@ +use juniper::{GraphQLObject, GraphQLUnion}; + +#[derive(GraphQLUnion)] +enum Character1 { + A { human: Human }, +} + +#[derive(GraphQLUnion)] +enum Character2 { + A(Human, u8), +} + +#[derive(GraphQLObject)] +pub struct Human { + id: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/enum_wrong_variant_field.stderr b/integration_tests/codegen_fail/fail/union/enum_wrong_variant_field.stderr new file mode 100644 index 00000000..4f5def6a --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_wrong_variant_field.stderr @@ -0,0 +1,15 @@ +error: GraphQL union enum allows only unnamed variants with a single field, e.g. `Some(T)` + --> $DIR/enum_wrong_variant_field.rs:5:5 + | +5 | A { human: Human }, + | ^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions + +error: GraphQL union enum allows only unnamed variants with a single field, e.g. `Some(T)` + --> $DIR/enum_wrong_variant_field.rs:10:6 + | +10 | A(Human, u8), + | ^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/codegen_fail/fail/union/impl_enum_field.rs b/integration_tests/codegen_fail/fail/union/impl_enum_field.rs deleted file mode 100644 index f3b67dd2..00000000 --- a/integration_tests/codegen_fail/fail/union/impl_enum_field.rs +++ /dev/null @@ -1,23 +0,0 @@ -#[derive(juniper::GraphQLEnum)] -#[graphql(context = ())] -pub enum Test { - A, - B, -} - -enum Character { - Test(Test), -} - -#[juniper::graphql_union] -impl Character { - fn resolve(&self) { - match self { - Test => match *self { - Character::Test(ref h) => Some(h), - }, - } - } -} - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/impl_enum_field.stderr b/integration_tests/codegen_fail/fail/union/impl_enum_field.stderr deleted file mode 100644 index 0337b998..00000000 --- a/integration_tests/codegen_fail/fail/union/impl_enum_field.stderr +++ /dev/null @@ -1,8 +0,0 @@ -error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType` is not satisfied - --> $DIR/impl_enum_field.rs:12:1 - | -12 | #[juniper::graphql_union] - | ^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType` is not implemented for `Test` - | - = note: required by `juniper::types::marker::GraphQLObjectType::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/union/impl_no_fields.rs b/integration_tests/codegen_fail/fail/union/impl_no_fields.rs deleted file mode 100644 index bafbe477..00000000 --- a/integration_tests/codegen_fail/fail/union/impl_no_fields.rs +++ /dev/null @@ -1,10 +0,0 @@ -enum Character {} - -#[juniper::graphql_union] -impl Character { - fn resolve(&self) { - match self {} - } -} - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/impl_no_fields.stderr b/integration_tests/codegen_fail/fail/union/impl_no_fields.stderr deleted file mode 100644 index 7eb1e9c7..00000000 --- a/integration_tests/codegen_fail/fail/union/impl_no_fields.stderr +++ /dev/null @@ -1,9 +0,0 @@ -error: GraphQL union expects at least one field - --> $DIR/impl_no_fields.rs:5:5 - | -5 | / fn resolve(&self) { -6 | | match self {} -7 | | } - | |_____^ - | - = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/codegen_fail/fail/union/impl_same_type.rs.disabled b/integration_tests/codegen_fail/fail/union/impl_same_type.rs.disabled deleted file mode 100644 index 4f070957..00000000 --- a/integration_tests/codegen_fail/fail/union/impl_same_type.rs.disabled +++ /dev/null @@ -1,32 +0,0 @@ -// NOTICE: This can not be tested. Implementing Into for each -// variant is not possible since we did not created the -// enum. Therefore, it is possible that the enum already has existing -// Into implementations. - -#[derive(juniper::GraphQLObject)] -pub struct Test { - test: String, -} - -enum Character { - A(Test), - B(Test), -} - -#[juniper::graphql_union] -impl Character { - fn resolve(&self) { - match self { - Test => match *self { - Character::A(ref h) => Some(h), - _ => None, - }, - Test => match *self { - Character::B(ref h) => Some(h), - _ => None, - }, - } - } -} - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/struct_name_double_underscored.rs b/integration_tests/codegen_fail/fail/union/struct_name_double_underscored.rs new file mode 100644 index 00000000..220bce55 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/struct_name_double_underscored.rs @@ -0,0 +1,18 @@ +use juniper::{GraphQLObject, GraphQLUnion}; + +#[derive(GraphQLUnion)] +#[graphql(on Human = __Character::a)] +struct __Character; + +impl __Character { + fn a(&self, _: &()) -> Option<&Human> { + None + } +} + +#[derive(GraphQLObject)] +pub struct Human { + id: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/struct_name_double_underscored.stderr b/integration_tests/codegen_fail/fail/union/struct_name_double_underscored.stderr new file mode 100644 index 00000000..48054c63 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/struct_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/struct_name_double_underscored.rs:5:8 + | +5 | struct __Character; + | ^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Schema diff --git a/integration_tests/codegen_fail/fail/union/struct_no_fields.rs b/integration_tests/codegen_fail/fail/union/struct_no_fields.rs new file mode 100644 index 00000000..51fb8899 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/struct_no_fields.rs @@ -0,0 +1,6 @@ +use juniper::GraphQLUnion; + +#[derive(GraphQLUnion)] +struct Character; + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/struct_no_fields.stderr b/integration_tests/codegen_fail/fail/union/struct_no_fields.stderr new file mode 100644 index 00000000..3d6aaeb2 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/struct_no_fields.stderr @@ -0,0 +1,7 @@ +error: GraphQL union expects at least one union variant + --> $DIR/struct_no_fields.rs:4:1 + | +4 | struct Character; + | ^^^^^^^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/codegen_fail/fail/union/struct_non_object_variant.rs b/integration_tests/codegen_fail/fail/union/struct_non_object_variant.rs new file mode 100644 index 00000000..553cde13 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/struct_non_object_variant.rs @@ -0,0 +1,19 @@ +use juniper::{GraphQLEnum, GraphQLUnion}; + +#[derive(GraphQLEnum)] +pub enum Test { + A, + B, +} + +#[derive(GraphQLUnion)] +#[graphql(on Test = Character::a)] +struct Character; + +impl Character { + fn a(&self, _: &()) -> Option<&Test> { + None + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/struct_non_object_variant.stderr b/integration_tests/codegen_fail/fail/union/struct_non_object_variant.stderr new file mode 100644 index 00000000..53d7ad7e --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/struct_non_object_variant.stderr @@ -0,0 +1,17 @@ +error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType` is not satisfied + --> $DIR/struct_non_object_variant.rs:9:10 + | +9 | #[derive(GraphQLUnion)] + | ^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType` is not implemented for `Test` + | + = note: required by `juniper::types::marker::GraphQLObjectType::mark` + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType<__S>` is not satisfied + --> $DIR/struct_non_object_variant.rs:9:10 + | +9 | #[derive(GraphQLUnion)] + | ^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType<__S>` is not implemented for `Test` + | + = note: required by `juniper::types::marker::GraphQLObjectType::mark` + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/union/struct_same_type_pretty.rs b/integration_tests/codegen_fail/fail/union/struct_same_type_pretty.rs new file mode 100644 index 00000000..9df11211 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/struct_same_type_pretty.rs @@ -0,0 +1,18 @@ +use juniper::GraphQLUnion; + +#[derive(GraphQLUnion)] +#[graphql(on i32 = Character::a)] +#[graphql(on i32 = Character::b)] +struct Character; + +impl Character { + fn a(&self, _: &()) -> Option<&i32> { + None + } + + fn b(&self, _: &()) -> Option<&i32> { + None + } +} + +fn main() {} 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 new file mode 100644 index 00000000..fca94bfb --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/struct_same_type_pretty.stderr @@ -0,0 +1,5 @@ +error: duplicated attribute + --> $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.rs b/integration_tests/codegen_fail/fail/union/struct_same_type_ugly.rs new file mode 100644 index 00000000..b1467262 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/struct_same_type_ugly.rs @@ -0,0 +1,18 @@ +use juniper::GraphQLUnion; + +#[derive(GraphQLUnion)] +#[graphql(on String = Character::a)] +#[graphql(on std::string::String = Character::b)] +struct Character; + +impl Character { + fn a(&self, _: &()) -> Option<&String> { + None + } + + fn b(&self, _: &()) -> Option<&String> { + None + } +} + +fn main() {} 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 new file mode 100644 index 00000000..e901067c --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/struct_same_type_ugly.stderr @@ -0,0 +1,10 @@ +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)] + | ^^^^^^^^^^^^ + | | + | first implementation here + | conflicting implementation for `std::string::String` + | + = 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/union/trait_fail_infer_context.rs b/integration_tests/codegen_fail/fail/union/trait_fail_infer_context.rs new file mode 100644 index 00000000..7077fe8c --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_fail_infer_context.rs @@ -0,0 +1,35 @@ +use juniper::{graphql_union, FromContext, GraphQLObject}; + +#[graphql_union] +trait Character { + fn a(&self, ctx: &SubContext) -> Option<&Human>; + fn b(&self, ctx: &CustomContext) -> Option<&Droid>; +} + +#[derive(GraphQLObject)] +#[graphql(context = CustomContext)] +pub struct Human { + id: String, + home_planet: String, +} + +#[derive(GraphQLObject)] +#[graphql(context = CustomContext)] +pub struct Droid { + id: String, + primary_function: String, +} + +pub struct CustomContext; +impl juniper::Context for CustomContext {} + +pub struct SubContext; +impl juniper::Context for SubContext {} + +impl FromContext for SubContext { + fn from(_: &CustomContext) -> &Self { + &Self + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/trait_fail_infer_context.stderr b/integration_tests/codegen_fail/fail/union/trait_fail_infer_context.stderr new file mode 100644 index 00000000..6ffa46c5 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_fail_infer_context.stderr @@ -0,0 +1,18 @@ +error[E0277]: the trait bound `CustomContext: juniper::executor::FromContext` is not satisfied + --> $DIR/trait_fail_infer_context.rs:3:1 + | +3 | #[graphql_union] + | ^^^^^^^^^^^^^^^^ the trait `juniper::executor::FromContext` is not implemented for `CustomContext` + | + = note: required by `juniper::executor::FromContext::from` + = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0308]: mismatched types + --> $DIR/trait_fail_infer_context.rs:3:1 + | +3 | #[graphql_union] + | ^^^^^^^^^^^^^^^^ expected struct `CustomContext`, found struct `SubContext` + | + = note: expected reference `&CustomContext` + found reference `&SubContext` + = 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/trait_method_conflicts_with_external_resolver_fn.rs b/integration_tests/codegen_fail/fail/union/trait_method_conflicts_with_external_resolver_fn.rs new file mode 100644 index 00000000..aff4b198 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_method_conflicts_with_external_resolver_fn.rs @@ -0,0 +1,13 @@ +use juniper::{graphql_union, GraphQLObject}; + +#[graphql_union(on Human = some_fn)] +trait Character { + fn a(&self) -> Option<&Human>; +} + +#[derive(GraphQLObject)] +pub struct Human { + id: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/trait_method_conflicts_with_external_resolver_fn.stderr b/integration_tests/codegen_fail/fail/union/trait_method_conflicts_with_external_resolver_fn.stderr new file mode 100644 index 00000000..0034185a --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_method_conflicts_with_external_resolver_fn.stderr @@ -0,0 +1,8 @@ +error: GraphQL union trait method `a` conflicts with the external resolver function `some_fn` declared on the trait to resolve the variant type `Human` + --> $DIR/trait_method_conflicts_with_external_resolver_fn.rs:5:5 + | +5 | fn a(&self) -> Option<&Human>; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions + = note: use `#[graphql_union(ignore)]` attribute to ignore this trait method for union variants resolution diff --git a/integration_tests/codegen_fail/fail/union/trait_name_double_underscored.rs b/integration_tests/codegen_fail/fail/union/trait_name_double_underscored.rs new file mode 100644 index 00000000..86cb5225 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_name_double_underscored.rs @@ -0,0 +1,13 @@ +use juniper::{graphql_union, GraphQLObject}; + +#[graphql_union] +trait __Character { + fn a(&self) -> Option<&Human>; +} + +#[derive(GraphQLObject)] +pub struct Human { + id: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/trait_name_double_underscored.stderr b/integration_tests/codegen_fail/fail/union/trait_name_double_underscored.stderr new file mode 100644 index 00000000..d010fee2 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_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/trait_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/union/trait_no_fields.rs b/integration_tests/codegen_fail/fail/union/trait_no_fields.rs new file mode 100644 index 00000000..a2b14cd2 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_no_fields.rs @@ -0,0 +1,6 @@ +use juniper::graphql_union; + +#[graphql_union] +trait Character {} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/trait_no_fields.stderr b/integration_tests/codegen_fail/fail/union/trait_no_fields.stderr new file mode 100644 index 00000000..8261623d --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_no_fields.stderr @@ -0,0 +1,7 @@ +error: GraphQL union expects at least one union variant + --> $DIR/trait_no_fields.rs:4:1 + | +4 | trait Character {} + | ^^^^^^^^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/codegen_fail/fail/union/trait_non_object_variant.rs b/integration_tests/codegen_fail/fail/union/trait_non_object_variant.rs new file mode 100644 index 00000000..4a1626b2 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_non_object_variant.rs @@ -0,0 +1,14 @@ +use juniper::{graphql_union, GraphQLEnum}; + +#[derive(GraphQLEnum)] +pub enum Test { + A, + B, +} + +#[graphql_union] +trait Character { + fn a(&self) -> Option<&Test>; +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/trait_non_object_variant.stderr b/integration_tests/codegen_fail/fail/union/trait_non_object_variant.stderr new file mode 100644 index 00000000..98e0193b --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_non_object_variant.stderr @@ -0,0 +1,17 @@ +error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType` is not satisfied + --> $DIR/trait_non_object_variant.rs:9:1 + | +9 | #[graphql_union] + | ^^^^^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType` is not implemented for `Test` + | + = note: required by `juniper::types::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 `Test: juniper::types::marker::GraphQLObjectType<__S>` is not satisfied + --> $DIR/trait_non_object_variant.rs:9:1 + | +9 | #[graphql_union] + | ^^^^^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType<__S>` is not implemented for `Test` + | + = note: required by `juniper::types::marker::GraphQLObjectType::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/union/trait_same_type_pretty.rs b/integration_tests/codegen_fail/fail/union/trait_same_type_pretty.rs new file mode 100644 index 00000000..e84f5eb9 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_same_type_pretty.rs @@ -0,0 +1,9 @@ +use juniper::graphql_union; + +#[graphql_union] +trait Character { + fn a(&self) -> Option<&u8>; + fn b(&self) -> Option<&u8>; +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/trait_same_type_pretty.stderr b/integration_tests/codegen_fail/fail/union/trait_same_type_pretty.stderr new file mode 100644 index 00000000..f899e307 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_same_type_pretty.stderr @@ -0,0 +1,10 @@ +error: GraphQL union must have a different type for each union variant + --> $DIR/trait_same_type_pretty.rs:4:1 + | +4 | / trait Character { +5 | | fn a(&self) -> Option<&u8>; +6 | | fn b(&self) -> Option<&u8>; +7 | | } + | |_^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/codegen_fail/fail/union/trait_same_type_ugly.rs b/integration_tests/codegen_fail/fail/union/trait_same_type_ugly.rs new file mode 100644 index 00000000..b6274ad2 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_same_type_ugly.rs @@ -0,0 +1,9 @@ +use juniper::graphql_union; + +#[graphql_union] +trait Character { + fn a(&self) -> Option<&String>; + fn b(&self) -> Option<&std::string::String>; +} + +fn main() {} 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 new file mode 100644 index 00000000..39684e76 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_same_type_ugly.stderr @@ -0,0 +1,10 @@ +error[E0119]: conflicting implementations of trait `<(dyn Character + std::marker::Send + std::marker::Sync + '__obj) as juniper::types::marker::GraphQLUnion>::mark::_::{{closure}}#0::MutuallyExclusive` for type `std::string::String`: + --> $DIR/trait_same_type_ugly.rs:3:1 + | +3 | #[graphql_union] + | ^^^^^^^^^^^^^^^^ + | | + | first implementation here + | conflicting implementation for `std::string::String` + | + = 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/union/trait_with_attr_on_method.rs b/integration_tests/codegen_fail/fail/union/trait_with_attr_on_method.rs new file mode 100644 index 00000000..730c6a6d --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_with_attr_on_method.rs @@ -0,0 +1,14 @@ +use juniper::{graphql_union, GraphQLObject}; + +#[graphql_union] +trait Character { + #[graphql_union(with = something)] + fn a(&self) -> Option<&Human>; +} + +#[derive(GraphQLObject)] +pub struct Human { + id: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/trait_with_attr_on_method.stderr b/integration_tests/codegen_fail/fail/union/trait_with_attr_on_method.stderr new file mode 100644 index 00000000..aba0ab2e --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_with_attr_on_method.stderr @@ -0,0 +1,8 @@ +error: GraphQL union cannot use #[graphql_union(with = ...)] attribute on a trait method + --> $DIR/trait_with_attr_on_method.rs:5:21 + | +5 | #[graphql_union(with = something)] + | ^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions + = note: instead use #[graphql_union(ignore)] on the method with #[graphql_union(on ... = ...)] on the trait itself diff --git a/integration_tests/codegen_fail/fail/union/trait_wrong_method_input_args.rs b/integration_tests/codegen_fail/fail/union/trait_wrong_method_input_args.rs new file mode 100644 index 00000000..da9ef3b6 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_wrong_method_input_args.rs @@ -0,0 +1,13 @@ +use juniper::{graphql_union, GraphQLObject}; + +#[graphql_union] +trait Character { + fn a(&self, ctx: &(), rand: u8) -> Option<&Human>; +} + +#[derive(GraphQLObject)] +pub struct Human { + id: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/trait_wrong_method_input_args.stderr b/integration_tests/codegen_fail/fail/union/trait_wrong_method_input_args.stderr new file mode 100644 index 00000000..9b2fa2d8 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_wrong_method_input_args.stderr @@ -0,0 +1,7 @@ +error: GraphQL union expects trait method to accept `&self` only and, optionally, `&Context` + --> $DIR/trait_wrong_method_input_args.rs:5:10 + | +5 | fn a(&self, ctx: &(), rand: u8) -> Option<&Human>; + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/codegen_fail/fail/union/trait_wrong_method_return_type.rs b/integration_tests/codegen_fail/fail/union/trait_wrong_method_return_type.rs new file mode 100644 index 00000000..b63f4353 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_wrong_method_return_type.rs @@ -0,0 +1,13 @@ +use juniper::{graphql_union, GraphQLObject}; + +#[graphql_union] +trait Character { + fn a(&self) -> &Human; +} + +#[derive(GraphQLObject)] +pub struct Human { + id: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/trait_wrong_method_return_type.stderr b/integration_tests/codegen_fail/fail/union/trait_wrong_method_return_type.stderr new file mode 100644 index 00000000..bbc40568 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_wrong_method_return_type.stderr @@ -0,0 +1,7 @@ +error: GraphQL union expects trait method return type to be `Option<&VariantType>` only + --> $DIR/trait_wrong_method_return_type.rs:5:20 + | +5 | fn a(&self) -> &Human; + | ^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/juniper_tests/Cargo.toml b/integration_tests/juniper_tests/Cargo.toml index 71901812..89357147 100644 --- a/integration_tests/juniper_tests/Cargo.toml +++ b/integration_tests/juniper_tests/Cargo.toml @@ -1,12 +1,13 @@ [package] name = "juniper_tests" version = "0.1.0" -publish = false edition = "2018" +publish = false [dependencies] -juniper = { path = "../../juniper" } +derive_more = "0.99.7" futures = "0.3.1" +juniper = { path = "../../juniper" } [dev-dependencies] serde_json = { version = "1" } diff --git a/integration_tests/juniper_tests/src/codegen/derive_union.rs b/integration_tests/juniper_tests/src/codegen/derive_union.rs deleted file mode 100644 index fcec2e74..00000000 --- a/integration_tests/juniper_tests/src/codegen/derive_union.rs +++ /dev/null @@ -1,275 +0,0 @@ -// Test for union's derive macro - -#[cfg(test)] -use fnv::FnvHashMap; - -#[cfg(test)] -use juniper::{ - self, execute, DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLType, RootNode, - Value, Variables, -}; - -#[derive(juniper::GraphQLObject)] -pub struct Human { - id: String, - home_planet: String, -} - -#[derive(juniper::GraphQLObject)] -pub struct Droid { - id: String, - primary_function: String, -} - -#[derive(juniper::GraphQLUnion)] -#[graphql(description = "A Collection of things")] -pub enum Character { - One(Human), - Two(Droid), -} - -// Context Test -pub struct CustomContext { - is_left: bool, -} - -impl juniper::Context for CustomContext {} - -#[derive(juniper::GraphQLObject)] -#[graphql(Context = CustomContext)] -pub struct HumanContext { - id: String, - home_planet: String, -} - -#[derive(juniper::GraphQLObject)] -#[graphql(Context = CustomContext)] -pub struct DroidContext { - id: String, - primary_function: String, -} - -/// A Collection of things -#[derive(juniper::GraphQLUnion)] -#[graphql(Context = CustomContext)] -pub enum CharacterContext { - One(HumanContext), - Two(DroidContext), -} - -// #[juniper::object] compatibility - -pub struct HumanCompat { - id: String, - home_planet: String, -} - -#[juniper::graphql_object] -impl HumanCompat { - fn id(&self) -> &String { - &self.id - } - - fn home_planet(&self) -> &String { - &self.home_planet - } -} - -pub struct DroidCompat { - id: String, - primary_function: String, -} - -#[juniper::graphql_object] -impl DroidCompat { - fn id(&self) -> &String { - &self.id - } - - fn primary_function(&self) -> &String { - &self.primary_function - } -} - -#[derive(juniper::GraphQLUnion)] -#[graphql(Context = CustomContext)] -pub enum DifferentContext { - A(DroidContext), - B(Droid), -} - -// NOTICE: this can not compile due to generic implementation of GraphQLType<__S> -// #[derive(juniper::GraphQLUnion)] -// pub enum CharacterCompatFail { -// One(HumanCompat), -// Two(DroidCompat), -// } - -/// A Collection of things -#[derive(juniper::GraphQLUnion)] -#[graphql(Scalar = juniper::DefaultScalarValue)] -pub enum CharacterCompat { - One(HumanCompat), - Two(DroidCompat), -} - -pub struct Query; - -#[juniper::graphql_object( - Context = CustomContext, -)] -impl Query { - fn context(&self, ctx: &CustomContext) -> CharacterContext { - if ctx.is_left { - HumanContext { - id: "human-32".to_string(), - home_planet: "earth".to_string(), - } - .into() - } else { - DroidContext { - id: "droid-99".to_string(), - primary_function: "run".to_string(), - } - .into() - } - } -} - -#[tokio::test] -async fn test_derived_union_doc_macro() { - assert_eq!( - >::name(&()), - Some("Character") - ); - - let mut registry: juniper::Registry = juniper::Registry::new(FnvHashMap::default()); - let meta = Character::meta(&(), &mut registry); - - assert_eq!(meta.name(), Some("Character")); - assert_eq!( - meta.description(), - Some(&"A Collection of things".to_string()) - ); -} - -#[tokio::test] -async fn test_derived_union_doc_string() { - assert_eq!( - >::name(&()), - Some("CharacterContext") - ); - - let mut registry: juniper::Registry = juniper::Registry::new(FnvHashMap::default()); - let meta = CharacterContext::meta(&(), &mut registry); - - assert_eq!(meta.name(), Some("CharacterContext")); - assert_eq!( - meta.description(), - Some(&"A Collection of things".to_string()) - ); -} - -#[tokio::test] -async fn test_derived_union_left() { - let doc = r#" - { - context { - ... on HumanContext { - humanId: id - homePlanet - } - ... on DroidContext { - droidId: id - primaryFunction - } - } - }"#; - - let schema = RootNode::new( - Query, - EmptyMutation::::new(), - EmptySubscription::::new(), - ); - - assert_eq!( - execute( - doc, - None, - &schema, - &Variables::new(), - &CustomContext { is_left: true } - ) - .await, - Ok(( - Value::object( - vec![( - "context", - Value::object( - vec![ - ("humanId", Value::scalar("human-32".to_string())), - ("homePlanet", Value::scalar("earth".to_string())), - ] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect() - ), - vec![] - )) - ); -} - -#[tokio::test] -async fn test_derived_union_right() { - let doc = r#" - { - context { - ... on HumanContext { - humanId: id - homePlanet - } - ... on DroidContext { - droidId: id - primaryFunction - } - } - }"#; - - let schema = RootNode::new( - Query, - EmptyMutation::::new(), - EmptySubscription::::new(), - ); - - assert_eq!( - execute( - doc, - None, - &schema, - &Variables::new(), - &CustomContext { is_left: false } - ) - .await, - Ok(( - Value::object( - vec![( - "context", - Value::object( - vec![ - ("droidId", Value::scalar("droid-99".to_string())), - ("primaryFunction", Value::scalar("run".to_string())), - ] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect() - ), - vec![] - )) - ); -} diff --git a/integration_tests/juniper_tests/src/codegen/impl_union.rs b/integration_tests/juniper_tests/src/codegen/impl_union.rs deleted file mode 100644 index 5ed28a3f..00000000 --- a/integration_tests/juniper_tests/src/codegen/impl_union.rs +++ /dev/null @@ -1,44 +0,0 @@ -// Trait. - -#[derive(juniper::GraphQLObject)] -struct Human { - id: String, - home_planet: String, -} - -#[derive(juniper::GraphQLObject)] -struct Droid { - id: String, - primary_function: String, -} - -trait Character { - fn as_human(&self) -> Option<&Human> { - None - } - fn as_droid(&self) -> Option<&Droid> { - None - } -} - -impl Character for Human { - fn as_human(&self) -> Option<&Human> { - Some(&self) - } -} - -impl Character for Droid { - fn as_droid(&self) -> Option<&Droid> { - Some(&self) - } -} - -#[juniper::graphql_union] -impl<'a> GraphQLUnion for &'a dyn Character { - fn resolve(&self) { - match self { - Human => self.as_human(), - Droid => self.as_droid(), - } - } -} diff --git a/integration_tests/juniper_tests/src/codegen/mod.rs b/integration_tests/juniper_tests/src/codegen/mod.rs index f2bd68dc..99d2b365 100644 --- a/integration_tests/juniper_tests/src/codegen/mod.rs +++ b/integration_tests/juniper_tests/src/codegen/mod.rs @@ -2,8 +2,8 @@ mod derive_enum; mod derive_input_object; mod derive_object; mod derive_object_with_raw_idents; -mod derive_union; mod impl_object; mod impl_scalar; -mod impl_union; 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 new file mode 100644 index 00000000..7377dbea --- /dev/null +++ b/integration_tests/juniper_tests/src/codegen/union_attr.rs @@ -0,0 +1,1077 @@ +//! Tests for `#[graphql_union]` macro. + +use juniper::{ + execute, graphql_object, graphql_union, graphql_value, DefaultScalarValue, EmptyMutation, + EmptySubscription, GraphQLObject, GraphQLType, RootNode, ScalarValue, Variables, +}; + +#[derive(GraphQLObject)] +struct Human { + id: String, + home_planet: String, +} + +#[derive(GraphQLObject)] +struct Droid { + id: String, + primary_function: String, +} + +#[derive(GraphQLObject)] +struct Ewok { + id: String, + funny: bool, +} + +pub enum CustomContext { + Human, + Droid, + Ewok, +} +impl juniper::Context for CustomContext {} + +#[derive(GraphQLObject)] +#[graphql(context = CustomContext)] +pub struct HumanCustomContext { + id: String, + home_planet: String, +} + +#[derive(GraphQLObject)] +#[graphql(context = CustomContext)] +pub struct DroidCustomContext { + id: String, + primary_function: String, +} + +#[derive(GraphQLObject)] +#[graphql(context = CustomContext)] +struct EwokCustomContext { + id: String, + funny: bool, +} + +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 trivial { + use super::*; + + #[graphql_union] + trait Character { + fn as_human(&self) -> Option<&Human> { + None + } + fn as_droid(&self) -> Option<&Droid> { + None + } + } + + impl Character for Human { + fn as_human(&self) -> Option<&Human> { + Some(&self) + } + } + + impl Character for Droid { + fn as_droid(&self) -> Option<&Droid> { + Some(&self) + } + } + + type DynCharacter<'a> = dyn Character + Send + Sync + 'a; + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Box> { + let ch: Box> = match self { + Self::Human => Box::new(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Box::new(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + }; + ch + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot::Droid); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn is_graphql_union() { + 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": "UNION"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_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 generic { + use super::*; + + #[graphql_union] + trait Character { + fn as_human(&self) -> Option<&Human> { + None + } + fn as_droid(&self) -> Option<&Droid> { + None + } + } + + impl Character for Human { + fn as_human(&self) -> Option<&Human> { + Some(&self) + } + } + + impl Character for Droid { + fn as_droid(&self) -> Option<&Droid> { + Some(&self) + } + } + + type DynCharacter<'a, A, B> = dyn Character + Send + Sync + 'a; + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Box> { + let ch: Box> = match self { + Self::Human => Box::new(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Box::new(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + }; + ch + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot::Droid); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_type_name_without_type_params() { + const DOC: &str = r#"{ + __type(name: "Character") { + name + } + }"#; + + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "Character"}}), vec![])), + ); + } +} + +mod description_from_doc_comments { + use super::*; + + /// Rust docs. + #[graphql_union] + trait Character { + fn as_human(&self) -> Option<&Human> { + None + } + } + + impl Character for Human { + fn as_human(&self) -> Option<&Human> { + Some(&self) + } + } + + type DynCharacter<'a> = dyn Character + Send + Sync + 'a; + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Box> { + Box::new(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }) + } + } + + #[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 uses_doc_comment_as_description() { + const DOC: &str = r#"{ + __type(name: "Character") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"description": "Rust docs."}}), + vec![], + )), + ); + } +} + +mod explicit_name_and_description { + use super::*; + + /// Rust docs. + #[graphql_union(name = "MyChar", desc = "My character.")] + trait Character { + fn as_human(&self) -> Option<&Human> { + None + } + } + + impl Character for Human { + fn as_human(&self) -> Option<&Human> { + Some(&self) + } + } + + type DynCharacter<'a> = dyn Character + Send + Sync + 'a; + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Box> { + Box::new(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }) + } + } + + #[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 uses_custom_name() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "MyChar"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_custom_description() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"description": "My character."}}), + vec![], + )), + ); + } +} + +mod explicit_scalar { + use super::*; + + #[graphql_union(scalar = DefaultScalarValue)] + trait Character { + fn as_human(&self) -> Option<&Human> { + None + } + fn as_droid(&self) -> Option<&Droid> { + None + } + } + + impl Character for Human { + fn as_human(&self) -> Option<&Human> { + Some(&self) + } + } + + impl Character for Droid { + fn as_droid(&self) -> Option<&Droid> { + Some(&self) + } + } + + type DynCharacter<'a> = dyn Character + Send + Sync + 'a; + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(scalar = DefaultScalarValue)] + impl QueryRoot { + fn character(&self) -> Box> { + let ch: Box> = match self { + Self::Human => Box::new(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Box::new(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + }; + ch + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema::<_, 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 resolves_droid() { + 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![], + )), + ); + } +} + +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::*; + + #[graphql_union(context = CustomContext)] + trait Character { + fn as_human(&self) -> Option<&HumanCustomContext> { + None + } + fn as_droid(&self) -> Option<&DroidCustomContext> { + None + } + } + + impl Character for HumanCustomContext { + fn as_human(&self) -> 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_methods { + use super::*; + + #[graphql_union] + trait Character { + fn as_human(&self) -> Option<&Human> { + None + } + #[graphql_union(ignore)] + fn ignored(&self) -> Option<&Ewok> { + None + } + #[graphql_union(skip)] + fn skipped(&self) {} + } + + impl Character for Human { + fn as_human(&self) -> Option<&Human> { + Some(&self) + } + } + + type DynCharacter<'a> = dyn Character + Send + Sync + 'a; + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Box> { + Box::new(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }) + } + } + + #[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 ignores_ewok() { + const DOC: &str = r#"{ + __type(name: "Character") { + possibleTypes { + name + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"possibleTypes": [{"name": "Human"}]}}), + vec![], + )), + ); + } +} + +mod external_resolver { + use super::*; + + #[graphql_union(context = Database)] + #[graphql_union(on Droid = DynCharacter::as_droid)] + trait Character { + fn as_human(&self) -> Option<&Human> { + None + } + } + + impl Character for Human { + fn as_human(&self) -> Option<&Human> { + Some(&self) + } + } + + impl Character for Droid {} + + type DynCharacter<'a> = dyn Character + Send + Sync + 'a; + + impl<'a> DynCharacter<'a> { + fn as_droid<'db>(&self, db: &'db Database) -> Option<&'db Droid> { + db.droid.as_ref() + } + } + + struct Database { + droid: Option, + } + impl juniper::Context for Database {} + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(context = Database)] + impl QueryRoot { + fn character(&self) -> Box> { + let ch: Box> = match self { + Self::Human => Box::new(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Box::new(Droid { + id: "?????".to_string(), + primary_function: "???".to_string(), + }), + }; + ch + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + 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 resolves_droid() { + let schema = schema(QueryRoot::Droid); + let db = Database { + droid: Some(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } +} + +mod full_featured { + use super::*; + + /// Rust doc. + #[graphql_union(name = "MyChar")] + #[graphql_union(description = "My character.")] + #[graphql_union(context = CustomContext, scalar = DefaultScalarValue)] + #[graphql_union(on EwokCustomContext = resolve_ewok)] + trait Character { + fn as_human(&self, _: &()) -> Option<&HumanCustomContext> { + None + } + fn as_droid(&self) -> Option<&DroidCustomContext> { + None + } + #[graphql_union(ignore)] + fn as_ewok(&self) -> Option<&EwokCustomContext> { + None + } + #[graphql_union(ignore)] + fn ignored(&self) {} + } + + impl Character for HumanCustomContext { + fn as_human(&self, _: &()) -> Option<&HumanCustomContext> { + Some(&self) + } + } + + impl Character for DroidCustomContext { + fn as_droid(&self) -> Option<&DroidCustomContext> { + Some(&self) + } + } + + impl Character for EwokCustomContext { + fn as_ewok(&self) -> Option<&EwokCustomContext> { + Some(&self) + } + } + + type DynCharacter<'a, T> = dyn Character + Send + Sync + 'a; + + fn resolve_ewok<'a, T>( + ewok: &'a DynCharacter<'a, T>, + _: &CustomContext, + ) -> Option<&'a EwokCustomContext> { + ewok.as_ewok() + } + + 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(), + }), + CustomContext::Ewok => Box::new(EwokCustomContext { + id: "ewok-1".to_string(), + funny: true, + }), + }; + ch + } + } + + const DOC: &str = r#"{ + character { + ... on HumanCustomContext { + humanId: id + homePlanet + } + ... on DroidCustomContext { + droidId: id + primaryFunction + } + ... on EwokCustomContext { + ewokId: id + funny + } + } + }"#; + + #[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![], + )), + ); + } + + #[tokio::test] + async fn resolves_ewok() { + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Ewok).await, + Ok(( + graphql_value!({"character": {"ewokId": "ewok-1", "funny": true}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_custom_name() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Ewok).await, + Ok((graphql_value!({"__type": {"name": "MyChar"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_custom_description() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Ewok).await, + Ok(( + graphql_value!({"__type": {"description": "My character."}}), + vec![], + )), + ); + } +} diff --git a/integration_tests/juniper_tests/src/codegen/union_derive.rs b/integration_tests/juniper_tests/src/codegen/union_derive.rs new file mode 100644 index 00000000..55d4c047 --- /dev/null +++ b/integration_tests/juniper_tests/src/codegen/union_derive.rs @@ -0,0 +1,1388 @@ +//! Tests for `#[derive(GraphQLUnion)]` macro. + +use std::marker::PhantomData; + +use juniper::{ + execute, graphql_object, graphql_value, DefaultScalarValue, EmptyMutation, EmptySubscription, + GraphQLObject, GraphQLType, GraphQLUnion, RootNode, ScalarValue, Variables, +}; + +#[derive(GraphQLObject)] +struct Human { + id: String, + home_planet: String, +} + +#[derive(GraphQLObject)] +struct Droid { + id: String, + primary_function: String, +} + +#[derive(GraphQLObject)] +struct Ewok { + id: String, + funny: bool, +} + +pub enum CustomContext { + Human, + Droid, + Ewok, +} +impl juniper::Context for CustomContext {} + +#[derive(GraphQLObject)] +#[graphql(context = CustomContext)] +pub struct HumanCustomContext { + id: String, + home_planet: String, +} + +#[derive(GraphQLObject)] +#[graphql(context = CustomContext)] +pub struct DroidCustomContext { + id: String, + primary_function: String, +} + +#[derive(GraphQLObject)] +#[graphql(context = CustomContext)] +struct EwokCustomContext { + id: String, + funny: bool, +} + +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 trivial_enum { + use super::*; + + #[derive(GraphQLUnion)] + enum Character { + A(Human), + B(Droid), + } + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Character { + match self { + Self::Human => Character::A(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Character::B(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + } + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot::Droid); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn is_graphql_union() { + 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": "UNION"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_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 generic_enum { + use super::*; + + #[derive(GraphQLUnion)] + enum Character { + A(Human), + B(Droid), + #[graphql(ignore)] + _State(A, B), + } + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Character { + match self { + Self::Human => Character::A(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Character::B(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + } + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot::Droid); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_type_name_without_type_params() { + const DOC: &str = r#"{ + __type(name: "Character") { + name + } + }"#; + + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "Character"}}), vec![])), + ); + } +} + +mod description_from_doc_comments { + use super::*; + + /// Rust docs. + #[derive(GraphQLUnion)] + enum Character { + A(Human), + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Character { + Character::A(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }) + } + } + + #[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 uses_doc_comment_as_description() { + const DOC: &str = r#"{ + __type(name: "Character") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"description": "Rust docs."}}), + vec![], + )), + ); + } +} + +mod explicit_name_and_description { + use super::*; + + /// Rust docs. + #[derive(GraphQLUnion)] + #[graphql(name = "MyChar", desc = "My character.")] + enum Character { + A(Human), + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Character { + Character::A(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }) + } + } + + #[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 uses_custom_name() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "MyChar"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_custom_description() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"description": "My character."}}), + vec![], + )), + ); + } +} + +mod explicit_scalar { + use super::*; + + #[derive(GraphQLUnion)] + #[graphql(scalar = DefaultScalarValue)] + enum Character { + A(Human), + B(Droid), + } + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Character { + match self { + Self::Human => Character::A(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Character::B(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + } + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema::<_, 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 resolves_droid() { + 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![], + )), + ); + } +} + +mod custom_context { + use super::*; + + #[derive(GraphQLUnion)] + #[graphql(context = CustomContext)] + enum Character { + A(HumanCustomContext), + B(DroidCustomContext), + } + + struct QueryRoot; + + #[graphql_object(context = CustomContext)] + impl QueryRoot { + fn character(&self, ctx: &CustomContext) -> Character { + match ctx { + CustomContext::Human => Character::A(HumanCustomContext { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + CustomContext::Droid => Character::B(DroidCustomContext { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + _ => unimplemented!(), + } + } + } + + 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 different_context { + use super::*; + + #[derive(GraphQLUnion)] + #[graphql(context = CustomContext)] + enum Character { + A(HumanCustomContext), + B(Droid), + } + + struct QueryRoot; + + #[graphql_object(context = CustomContext)] + impl QueryRoot { + fn character(&self, ctx: &CustomContext) -> Character { + match ctx { + CustomContext::Human => Character::A(HumanCustomContext { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + CustomContext::Droid => Character::B(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + _ => unimplemented!(), + } + } + } + + const DOC: &str = r#"{ + character { + ... on HumanCustomContext { + humanId: id + homePlanet + } + ... on Droid { + 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_enum_variants { + use super::*; + + #[derive(GraphQLUnion)] + enum Character { + A(Human), + #[graphql(ignore)] + _C(Ewok), + #[graphql(skip)] + _D, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Character { + Character::A(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }) + } + } + + #[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 ignores_ewok() { + const DOC: &str = r#"{ + __type(name: "Character") { + possibleTypes { + name + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"possibleTypes": [{"name": "Human"}]}}), + vec![], + )), + ); + } +} + +mod external_resolver_enum { + use super::*; + + #[derive(GraphQLUnion)] + #[graphql(context = Database)] + #[graphql(on Droid = Character::as_droid)] + enum Character { + A(Human), + #[graphql(ignore)] + B, + } + + impl Character { + fn as_droid<'db>(&self, db: &'db Database) -> Option<&'db Droid> { + if let Self::B = self { + db.droid.as_ref() + } else { + None + } + } + } + + struct Database { + droid: Option, + } + impl juniper::Context for Database {} + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(context = Database)] + impl QueryRoot { + fn character(&self) -> Character { + match self { + Self::Human => Character::A(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Character::B, + } + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + 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 resolves_droid() { + let schema = schema(QueryRoot::Droid); + let db = Database { + droid: Some(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } +} + +mod external_resolver_enum_variant { + use super::*; + + #[derive(GraphQLUnion)] + #[graphql(context = Database)] + enum Character { + A(Human), + #[graphql(with = Character::as_droid)] + B(Droid), + } + + impl Character { + fn as_droid<'db>(&self, db: &'db Database) -> Option<&'db Droid> { + if let Self::B(_) = self { + db.droid.as_ref() + } else { + None + } + } + } + + struct Database { + droid: Option, + } + impl juniper::Context for Database {} + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(context = Database)] + impl QueryRoot { + fn character(&self) -> Character { + match self { + Self::Human => Character::A(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Character::B(Droid { + id: "?????".to_string(), + primary_function: "???".to_string(), + }), + } + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + 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 resolves_droid() { + let schema = schema(QueryRoot::Droid); + let db = Database { + droid: Some(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } +} + +mod full_featured_enum { + use super::*; + + /// Rust doc. + #[derive(GraphQLUnion)] + #[graphql(name = "MyChar")] + #[graphql(description = "My character.")] + #[graphql(context = CustomContext, scalar = DefaultScalarValue)] + #[graphql(on EwokCustomContext = resolve_ewok)] + enum Character { + A(HumanCustomContext), + #[graphql(with = Character::as_droid)] + B(DroidCustomContext), + #[graphql(ignore)] + C(EwokCustomContext), + #[graphql(ignore)] + _State(T), + } + + impl Character { + fn as_droid(&self, ctx: &CustomContext) -> Option<&DroidCustomContext> { + if let CustomContext::Droid = ctx { + if let Self::B(droid) = self { + return Some(droid); + } + } + None + } + } + + fn resolve_ewok<'a, T>( + ewok: &'a Character, + _: &CustomContext, + ) -> Option<&'a EwokCustomContext> { + if let Character::C(ewok) = ewok { + Some(ewok) + } else { + None + } + } + + struct QueryRoot; + + #[graphql_object(context = CustomContext)] + impl QueryRoot { + fn character(&self, ctx: &CustomContext) -> Character<()> { + match ctx { + CustomContext::Human => Character::A(HumanCustomContext { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + CustomContext::Droid => Character::B(DroidCustomContext { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + CustomContext::Ewok => Character::C(EwokCustomContext { + id: "ewok-1".to_string(), + funny: true, + }), + } + } + } + + const DOC: &str = r#"{ + character { + ... on HumanCustomContext { + humanId: id + homePlanet + } + ... on DroidCustomContext { + droidId: id + primaryFunction + } + ... on EwokCustomContext { + ewokId: id + funny + } + } + }"#; + + #[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![], + )), + ); + } + + #[tokio::test] + async fn resolves_ewok() { + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Ewok).await, + Ok(( + graphql_value!({"character": {"ewokId": "ewok-1", "funny": true}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_custom_name() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Ewok).await, + Ok((graphql_value!({"__type": {"name": "MyChar"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_custom_description() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Ewok).await, + Ok(( + graphql_value!({"__type": {"description": "My character."}}), + vec![], + )), + ); + } +} + +mod trivial_struct { + use super::*; + + #[derive(GraphQLUnion)] + #[graphql(context = Database)] + #[graphql( + on Human = Character::as_human, + on Droid = Character::as_droid, + )] + struct Character { + id: String, + } + + impl Character { + fn as_human<'db>(&self, db: &'db Database) -> Option<&'db Human> { + if let Some(human) = &db.human { + if human.id == self.id { + return Some(human); + } + } + None + } + + fn as_droid<'db>(&self, db: &'db Database) -> Option<&'db Droid> { + if let Some(droid) = &db.droid { + if droid.id == self.id { + return Some(droid); + } + } + None + } + } + + struct Database { + human: Option, + droid: Option, + } + impl juniper::Context for Database {} + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(context = Database)] + impl QueryRoot { + fn character(&self) -> Character { + Character { + id: match self { + Self::Human => "human-32", + Self::Droid => "droid-99", + } + .to_string(), + } + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + let db = Database { + human: Some(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + droid: None, + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot::Droid); + let db = Database { + human: None, + droid: Some(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } +} + +mod generic_struct { + use super::*; + + #[derive(GraphQLUnion)] + #[graphql(context = Database)] + #[graphql(on Human = Character::as_human)] + struct Character { + id: String, + _s: PhantomData<(A, B)>, + } + + impl Character { + fn as_human<'db>(&self, db: &'db Database) -> Option<&'db Human> { + if let Some(human) = &db.human { + if human.id == self.id { + return Some(human); + } + } + None + } + } + + struct Database { + human: Option, + } + impl juniper::Context for Database {} + + struct QueryRoot; + + #[graphql_object(context = Database)] + impl QueryRoot { + fn character(&self) -> Character { + Character { + id: "human-32".to_string(), + _s: PhantomData, + } + } + } + + #[tokio::test] + async fn resolves_human() { + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + } + }"#; + + let schema = schema(QueryRoot); + let db = Database { + human: Some(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_type_name_without_type_params() { + const DOC: &str = r#"{ + __type(name: "Character") { + name + } + }"#; + + let schema = schema(QueryRoot); + let db = Database { human: None }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok((graphql_value!({"__type": {"name": "Character"}}), vec![])), + ); + } +} + +mod full_featured_struct { + use super::*; + + /// Rust doc. + #[derive(GraphQLUnion)] + #[graphql(name = "MyChar")] + #[graphql(description = "My character.")] + #[graphql(context = Database, scalar = DefaultScalarValue)] + #[graphql(on Human = Character::as_human)] + #[graphql(on Droid = Character::as_droid)] + struct Character { + id: String, + _s: PhantomData, + } + + impl Character { + fn as_human<'db>(&self, db: &'db Database) -> Option<&'db Human> { + if let Some(human) = &db.human { + if human.id == self.id { + return Some(human); + } + } + None + } + } + + impl Character { + fn as_droid<'db>(&self, db: &'db Database) -> Option<&'db Droid> { + if let Some(droid) = &db.droid { + if droid.id == self.id { + return Some(droid); + } + } + None + } + } + + struct Database { + human: Option, + droid: Option, + } + impl juniper::Context for Database {} + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(context = Database)] + impl QueryRoot { + fn character(&self) -> Character<()> { + Character { + id: match self { + Self::Human => "human-32", + Self::Droid => "droid-99", + } + .to_string(), + _s: PhantomData, + } + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + let db = Database { + human: Some(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + droid: None, + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot::Droid); + let db = Database { + human: None, + droid: Some(Droid { + id: "droid-99".to_string(), + primary_function: "run".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 uses_custom_name() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + name + } + }"#; + + 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!({"__type": {"name": "MyChar"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_custom_description() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + description + } + }"#; + + 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!({"__type": {"description": "My character."}}), + vec![], + )), + ); + } +} diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 35e6dabf..154c06d0 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -2,7 +2,7 @@ ## Features -- Support raw identifiers in field and argument names. (#[object] macro) +- Support raw identifiers in field and argument names. (`#[object]` macro) - Most error types now implement `std::error::Error`: - `GraphQLError` @@ -29,10 +29,21 @@ See [#618](https://github.com/graphql-rust/juniper/pull/618). - Derive macro `GraphQLEnum` supports custom context (see [#621](https://github.com/graphql-rust/juniper/pull/621)) +- Reworked `#[derive(GraphQLUnion)]` macro ([#666]): + - Applicable to enums and structs. + - Supports custom resolvers. + - Supports generics. + - Supports multiple `#[graphql]` attributes. +- New `#[graphql_union]` macro ([#666]): + - Applicable to traits. + - Supports custom resolvers. + - Supports generics. + - Supports multiple `#[graphql_union]` attributes. + - Better error messages for all proc macros (see [#631](https://github.com/graphql-rust/juniper/pull/631) -- Improved lookahead visibility for aliased fields (see [#662](https://github.com/graphql-rust/juniper/pull/631)) +- Improved lookahead visibility for aliased fields (see [#662](https://github.com/graphql-rust/juniper/pull/631)) ## Breaking Changes @@ -45,10 +56,10 @@ See [#618](https://github.com/graphql-rust/juniper/pull/618). - Remove deprecated `ScalarValue` custom derive (renamed to GraphQLScalarValue) -- `graphql_union!` macro removed, replaced by `#[graphql_union]` proc macro +- `graphql_union!` macro removed, replaced by `#[graphql_union]` proc macro and custom resolvers for the `#[derive(GraphQLUnion)]` macro. +- The `#[derive(GraphQLUnion)]` macro doesn't generate `From` impls for enum variants anymore. Consider using the [`derive_more`](https//docs.rs/derive_more) crate directly ([#666]). -- ScalarRefValue trait removed - Trait was not required. +- `ScalarRefValue` trait removed. Trait was not required. - Changed return type of GraphQLType::resolve to `ExecutionResult` This was done to unify the return type of all resolver methods @@ -59,7 +70,7 @@ See [#618](https://github.com/graphql-rust/juniper/pull/618). add subscription type to `RootNode`, add subscription endpoint to `playground_source()` -- Putting a scalar type into a string is not allowed anymore, e..g, +- Putting a scalar type into a string is not allowed anymore, e.g. `#[graphql(scalar = "DefaultScalarValue")]`. Only `#[derive(GraphQLInputObject)]` supported this syntax. The refactoring of GraphQLInputObject allowed to drop the support @@ -75,6 +86,8 @@ See [#618](https://github.com/graphql-rust/juniper/pull/618). - When using LookAheadMethods to access child selections, children are always found using their alias if it exists rather than their name (see [#662](https://github.com/graphql-rust/juniper/pull/631)). These methods are also deprecated in favour of the new `children` method. +[#666]: https://github.com/graphql-rust/juniper/pull/666 + # [[0.14.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper-0.14.2) - Fix incorrect validation with non-executed operations [#455](https://github.com/graphql-rust/juniper/issues/455) diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index 69e970a6..a8bd7b46 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -43,6 +43,7 @@ futures = "0.3.1" indexmap = { version = "1.0.0", features = ["serde-1"] } serde = { version = "1.0.8", features = ["derive"] } serde_json = { version="1.0.2", optional = true } +static_assertions = "1.1" url = { version = "2", optional = true } uuid = { version = "0.8", optional = true } diff --git a/juniper/src/executor/mod.rs b/juniper/src/executor/mod.rs index d1378c17..998c7d50 100644 --- a/juniper/src/executor/mod.rs +++ b/juniper/src/executor/mod.rs @@ -242,8 +242,9 @@ impl IntoFieldError for FieldError { } #[doc(hidden)] -pub trait IntoResolvable<'a, S, T: GraphQLType, C>: Sized +pub trait IntoResolvable<'a, S, T, C> where + T: GraphQLType, S: ScalarValue, { #[doc(hidden)] @@ -404,7 +405,7 @@ where pub fn resolve_with_ctx(&self, info: &T::TypeInfo, value: &T) -> ExecutionResult where NewCtxT: FromContext, - T: GraphQLType, + T: GraphQLType + ?Sized, { self.replaced_context(>::from(self.context)) .resolve(info, value) @@ -413,7 +414,7 @@ where /// Resolve a single arbitrary value into an `ExecutionResult` pub fn resolve(&self, info: &T::TypeInfo, value: &T) -> ExecutionResult where - T: GraphQLType, + T: GraphQLType + ?Sized, { value.resolve(info, self.current_selection_set, self) } @@ -421,7 +422,7 @@ where /// Resolve a single arbitrary value into an `ExecutionResult` pub async fn resolve_async(&self, info: &T::TypeInfo, value: &T) -> ExecutionResult where - T: crate::GraphQLTypeAsync + Send + Sync, + T: crate::GraphQLTypeAsync + Send + Sync + ?Sized, T::TypeInfo: Send + Sync, CtxT: Send + Sync, S: Send + Sync, @@ -468,18 +469,15 @@ where /// If the field fails to resolve, `null` will be returned. pub async fn resolve_into_value_async(&self, info: &T::TypeInfo, value: &T) -> Value where - T: crate::GraphQLTypeAsync + Send + Sync, + T: crate::GraphQLTypeAsync + Send + Sync + ?Sized, T::TypeInfo: Send + Sync, CtxT: Send + Sync, S: Send + Sync, { - match self.resolve_async(info, value).await { - Ok(v) => v, - Err(e) => { - self.push_error(e); - Value::null() - } - } + self.resolve_async(info, value).await.unwrap_or_else(|e| { + self.push_error(e); + Value::null() + }) } /// Derive a new executor by replacing the context @@ -1103,7 +1101,7 @@ where /// construct its metadata and store it. pub fn get_type(&mut self, info: &T::TypeInfo) -> Type<'r> where - T: GraphQLType, + T: GraphQLType + ?Sized, { if let Some(name) = T::name(info) { let validated_name = name.parse::().unwrap(); @@ -1124,7 +1122,7 @@ where /// Create a field with the provided name pub fn field(&mut self, name: &str, info: &T::TypeInfo) -> Field<'r, S> where - T: GraphQLType, + T: GraphQLType + ?Sized, { Field { name: name.to_owned(), @@ -1156,7 +1154,7 @@ where /// Create an argument with the provided name pub fn arg(&mut self, name: &str, info: &T::TypeInfo) -> Argument<'r, S> where - T: GraphQLType + FromInputValue, + T: GraphQLType + FromInputValue + ?Sized, { Argument::new(name, self.get_type::(info)) } @@ -1172,7 +1170,7 @@ where info: &T::TypeInfo, ) -> Argument<'r, S> where - T: GraphQLType + ToInputValue + FromInputValue, + T: GraphQLType + ToInputValue + FromInputValue + ?Sized, { Argument::new(name, self.get_type::>(info)).default_value(value.to_input_value()) } @@ -1188,20 +1186,23 @@ where /// This expects the type to implement `FromInputValue`. pub fn build_scalar_type(&mut self, info: &T::TypeInfo) -> ScalarMeta<'r, S> where - T: FromInputValue + GraphQLType + ParseScalarValue + 'r, + T: FromInputValue + GraphQLType + ParseScalarValue + ?Sized + 'r, { let name = T::name(info).expect("Scalar types must be named. Implement name()"); ScalarMeta::new::(Cow::Owned(name.to_string())) } /// Create a list meta type - pub fn build_list_type>(&mut self, info: &T::TypeInfo) -> ListMeta<'r> { + pub fn build_list_type + ?Sized>( + &mut self, + info: &T::TypeInfo, + ) -> ListMeta<'r> { let of_type = self.get_type::(info); ListMeta::new(of_type) } /// Create a nullable meta type - pub fn build_nullable_type>( + pub fn build_nullable_type + ?Sized>( &mut self, info: &T::TypeInfo, ) -> NullableMeta<'r> { @@ -1219,7 +1220,7 @@ where fields: &[Field<'r, S>], ) -> ObjectMeta<'r, S> where - T: GraphQLType, + T: GraphQLType + ?Sized, { let name = T::name(info).expect("Object types must be named. Implement name()"); @@ -1235,7 +1236,7 @@ where values: &[EnumValue], ) -> EnumMeta<'r, S> where - T: FromInputValue + GraphQLType, + T: FromInputValue + GraphQLType + ?Sized, { let name = T::name(info).expect("Enum types must be named. Implement name()"); @@ -1250,7 +1251,7 @@ where fields: &[Field<'r, S>], ) -> InterfaceMeta<'r, S> where - T: GraphQLType, + T: GraphQLType + ?Sized, { let name = T::name(info).expect("Interface types must be named. Implement name()"); @@ -1262,7 +1263,7 @@ where /// Create a union meta type pub fn build_union_type(&mut self, info: &T::TypeInfo, types: &[Type<'r>]) -> UnionMeta<'r> where - T: GraphQLType, + T: GraphQLType + ?Sized, { let name = T::name(info).expect("Union types must be named. Implement name()"); @@ -1276,7 +1277,7 @@ where args: &[Argument<'r, S>], ) -> InputObjectMeta<'r, S> where - T: FromInputValue + GraphQLType, + T: FromInputValue + GraphQLType + ?Sized, { let name = T::name(info).expect("Input object types must be named. Implement name()"); diff --git a/juniper/src/executor_tests/interfaces_unions.rs b/juniper/src/executor_tests/interfaces_unions.rs index 51a17ed9..54fcad30 100644 --- a/juniper/src/executor_tests/interfaces_unions.rs +++ b/juniper/src/executor_tests/interfaces_unions.rs @@ -167,6 +167,7 @@ mod union { value::Value, }; + #[crate::graphql_union_internal] trait Pet { fn as_dog(&self) -> Option<&Dog> { None @@ -176,16 +177,6 @@ mod union { } } - #[crate::graphql_union_internal] - impl<'a> GraphQLUnion for &'a dyn Pet { - fn resolve(&self) { - match self { - Dog => self.as_dog(), - Cat => self.as_cat(), - } - } - } - struct Dog { name: String, woofs: bool, diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index 32e26f3b..14bf1fa5 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -93,6 +93,8 @@ Juniper has not reached 1.0 yet, thus some API instability should be expected. #![doc(html_root_url = "https://docs.rs/juniper/0.14.2")] #![warn(missing_docs)] +use std::fmt; + #[doc(hidden)] pub extern crate serde; @@ -111,6 +113,13 @@ extern crate uuid; #[cfg(any(test, feature = "bson"))] extern crate bson; +// These are required by the code generated via the `juniper_codegen` macros. +#[doc(hidden)] +pub use {futures, static_assertions as sa}; + +#[doc(inline)] +pub use futures::future::BoxFuture; + // Depend on juniper_codegen and re-export everything in it. // This allows users to just depend on juniper and get the derive // functionality automatically. @@ -124,7 +133,7 @@ pub use juniper_codegen::{ use juniper_codegen::{ graphql_object_internal, graphql_scalar_internal, graphql_subscription_internal, graphql_union_internal, GraphQLEnumInternal, GraphQLInputObjectInternal, - GraphQLScalarValueInternal, + GraphQLScalarValueInternal, GraphQLUnionInternal, }; #[macro_use] @@ -161,7 +170,6 @@ use crate::{ parser::{parse_document_source, ParseError, Spanning}, validation::{validate_input_values, visit_all_rules, ValidatorContext}, }; -use std::fmt; pub use crate::{ ast::{FromInputValue, InputValue, Selection, ToInputValue, Type}, @@ -179,7 +187,7 @@ pub use crate::{ types::{ async_await::GraphQLTypeAsync, base::{Arguments, GraphQLType, TypeKind}, - marker, + marker::{self, GraphQLUnion}, scalars::{EmptyMutation, EmptySubscription, ID}, subscriptions::{GraphQLSubscriptionType, SubscriptionConnection, SubscriptionCoordinator}, }, @@ -187,9 +195,6 @@ pub use crate::{ value::{DefaultScalarValue, Object, ParseScalarResult, ParseScalarValue, ScalarValue, Value}, }; -/// A pinned, boxed future that can be polled. -pub type BoxFuture<'a, T> = std::pin::Pin + 'a + Send>>; - /// An error that prevented query execution #[derive(Debug, PartialEq)] #[allow(missing_docs)] diff --git a/juniper/src/macros/tests/union.rs b/juniper/src/macros/tests/union.rs index 6dfbfe75..9db321ee 100644 --- a/juniper/src/macros/tests/union.rs +++ b/juniper/src/macros/tests/union.rs @@ -14,81 +14,70 @@ use std::marker::PhantomData; use crate::{ ast::InputValue, + graphql_object_internal, schema::model::RootNode, types::scalars::{EmptyMutation, EmptySubscription}, value::{DefaultScalarValue, Object, Value}, + GraphQLUnionInternal, }; struct Concrete; -enum CustomName { - Concrete(Concrete), -} - -enum WithLifetime<'a> { - Int(PhantomData<&'a i32>), -} -enum WithGenerics { - Generic(T), -} - -enum DescriptionFirst { - Concrete(Concrete), -} - -struct Root; - -#[crate::graphql_object_internal] +#[graphql_object_internal] impl Concrete { fn simple() -> i32 { 123 } } -#[crate::graphql_union_internal(name = "ACustomNamedUnion")] -impl CustomName { - fn resolve(&self) { - match self { - Concrete => match *self { - CustomName::Concrete(ref c) => Some(c), - }, - } - } +#[derive(GraphQLUnionInternal)] +#[graphql(name = "ACustomNamedUnion", scalar = DefaultScalarValue)] +enum CustomName { + Concrete(Concrete), +} + +#[derive(GraphQLUnionInternal)] +#[graphql(on Concrete = WithLifetime::resolve, scalar = DefaultScalarValue)] +enum WithLifetime<'a> { + #[graphql(ignore)] + Int(PhantomData<&'a i32>), } -#[crate::graphql_union_internal] impl<'a> WithLifetime<'a> { - fn resolve(&self) { - match self { - Concrete => match *self { - WithLifetime::Int(_) => Some(&Concrete), - }, + fn resolve(&self, _: &()) -> Option<&Concrete> { + if matches!(self, Self::Int(_)) { + Some(&Concrete) + } else { + None } } } -#[crate::graphql_union_internal] +#[derive(GraphQLUnionInternal)] +#[graphql(on Concrete = WithGenerics::resolve, scalar = DefaultScalarValue)] +enum WithGenerics { + #[graphql(ignore)] + Generic(T), +} + impl WithGenerics { - fn resolve(&self) { - match self { - Concrete => match *self { - WithGenerics::Generic(_) => Some(&Concrete), - }, + fn resolve(&self, _: &()) -> Option<&Concrete> { + if matches!(self, Self::Generic(_)) { + Some(&Concrete) + } else { + None } } } -#[crate::graphql_union_internal(description = "A description")] -impl DescriptionFirst { - fn resolve(&self) { - match self { - Concrete => match *self { - DescriptionFirst::Concrete(ref c) => Some(c), - }, - } - } +#[derive(GraphQLUnionInternal)] +#[graphql(description = "A description", scalar = DefaultScalarValue)] +enum DescriptionFirst { + Concrete(Concrete), } +struct Root; + // FIXME: make async work #[crate::graphql_object_internal(noasync)] impl<'a> Root { diff --git a/juniper/src/schema/meta.rs b/juniper/src/schema/meta.rs index d8621035..b024ed05 100644 --- a/juniper/src/schema/meta.rs +++ b/juniper/src/schema/meta.rs @@ -572,8 +572,10 @@ where S: ScalarValue, { /// Build a new input type with the specified name and input fields - pub fn new>(name: Cow<'a, str>, input_fields: &[Argument<'a, S>]) -> Self -where { + pub fn new(name: Cow<'a, str>, input_fields: &[Argument<'a, S>]) -> Self + where + T: FromInputValue + ?Sized, + { InputObjectMeta { name, description: None, diff --git a/juniper/src/types/async_await.rs b/juniper/src/types/async_await.rs index ef5f9ec4..156c2ab5 100644 --- a/juniper/src/types/async_await.rs +++ b/juniper/src/types/async_await.rs @@ -98,7 +98,7 @@ fn resolve_selection_set_into_async<'a, 'e, T, CtxT, S>( executor: &'e Executor<'e, 'e, CtxT, S>, ) -> BoxFuture<'a, Value> where - T: GraphQLTypeAsync, + T: GraphQLTypeAsync + ?Sized, T::TypeInfo: Send + Sync, S: ScalarValue + Send + Sync, CtxT: Send + Sync, @@ -129,7 +129,7 @@ pub(crate) async fn resolve_selection_set_into_async_recursive<'a, T, CtxT, S>( executor: &'a Executor<'a, 'a, CtxT, S>, ) -> Value where - T: GraphQLTypeAsync + Send + Sync, + T: GraphQLTypeAsync + Send + Sync + ?Sized, T::TypeInfo: Send + Sync, S: ScalarValue + Send + Sync, CtxT: Send + Sync, diff --git a/juniper/src/types/base.rs b/juniper/src/types/base.rs index 1ce3754f..c80188ed 100644 --- a/juniper/src/types/base.rs +++ b/juniper/src/types/base.rs @@ -230,7 +230,7 @@ impl GraphQLType for User ``` */ -pub trait GraphQLType: Sized +pub trait GraphQLType where S: ScalarValue, { @@ -355,7 +355,7 @@ pub(crate) fn resolve_selection_set_into( result: &mut Object, ) -> bool where - T: GraphQLType, + T: GraphQLType + ?Sized, S: ScalarValue, { let meta_type = executor diff --git a/juniper/src/types/containers.rs b/juniper/src/types/containers.rs index e587dee5..58962402 100644 --- a/juniper/src/types/containers.rs +++ b/juniper/src/types/containers.rs @@ -1,15 +1,11 @@ use crate::{ ast::{FromInputValue, InputValue, Selection, ToInputValue}, - executor::ExecutionResult, + executor::{ExecutionResult, Executor, Registry}, schema::meta::MetaType, + types::{async_await::GraphQLTypeAsync, base::GraphQLType}, value::{ScalarValue, Value}, }; -use crate::{ - executor::{Executor, Registry}, - types::base::GraphQLType, -}; - impl GraphQLType for Option where S: ScalarValue, @@ -42,6 +38,30 @@ where } } +impl GraphQLTypeAsync for Option +where + T: GraphQLTypeAsync, + T::TypeInfo: Send + Sync, + S: ScalarValue + Send + Sync, + CtxT: Send + Sync, +{ + fn resolve_async<'a>( + &'a self, + info: &'a Self::TypeInfo, + _selection_set: Option<&'a [Selection]>, + executor: &'a Executor, + ) -> crate::BoxFuture<'a, ExecutionResult> { + let f = async move { + let value = match *self { + Some(ref obj) => executor.resolve_into_value_async(info, obj).await, + None => Value::null(), + }; + Ok(value) + }; + Box::pin(f) + } +} + impl FromInputValue for Option where T: FromInputValue, @@ -50,10 +70,7 @@ where fn from_input_value<'a>(v: &'a InputValue) -> Option> { match v { &InputValue::Null => Some(None), - v => match v.convert() { - Some(x) => Some(Some(x)), - None => None, - }, + v => v.convert().map(Some), } } } @@ -100,6 +117,24 @@ where } } +impl GraphQLTypeAsync for Vec +where + T: GraphQLTypeAsync, + T::TypeInfo: Send + Sync, + S: ScalarValue + Send + Sync, + CtxT: Send + Sync, +{ + fn resolve_async<'a>( + &'a self, + info: &'a Self::TypeInfo, + _selection_set: Option<&'a [Selection]>, + executor: &'a Executor, + ) -> crate::BoxFuture<'a, ExecutionResult> { + let f = resolve_into_list_async(executor, info, self.iter()); + Box::pin(f) + } +} + impl FromInputValue for Vec where T: FromInputValue, @@ -117,13 +152,7 @@ where { None } } - ref other => { - if let Some(e) = other.convert() { - Some(vec![e]) - } else { - None - } - } + ref other => other.convert().map(|e| vec![e]), } } } @@ -134,11 +163,11 @@ where S: ScalarValue, { fn to_input_value(&self) -> InputValue { - InputValue::list(self.iter().map(|v| v.to_input_value()).collect()) + InputValue::list(self.iter().map(T::to_input_value).collect()) } } -impl<'a, S, T, CtxT> GraphQLType for &'a [T] +impl GraphQLType for [T] where S: ScalarValue, T: GraphQLType, @@ -167,25 +196,43 @@ where } } +impl GraphQLTypeAsync for [T] +where + T: GraphQLTypeAsync, + T::TypeInfo: Send + Sync, + S: ScalarValue + Send + Sync, + CtxT: Send + Sync, +{ + fn resolve_async<'a>( + &'a self, + info: &'a Self::TypeInfo, + _selection_set: Option<&'a [Selection]>, + executor: &'a Executor, + ) -> crate::BoxFuture<'a, ExecutionResult> { + let f = resolve_into_list_async(executor, info, self.iter()); + Box::pin(f) + } +} + impl<'a, T, S> ToInputValue for &'a [T] where T: ToInputValue, S: ScalarValue, { fn to_input_value(&self) -> InputValue { - InputValue::list(self.iter().map(|v| v.to_input_value()).collect()) + InputValue::list(self.iter().map(T::to_input_value).collect()) } } -fn resolve_into_list( +fn resolve_into_list<'t, S, T, I>( executor: &Executor, info: &T::TypeInfo, iter: I, ) -> ExecutionResult where S: ScalarValue, - I: Iterator + ExactSizeIterator, - T: GraphQLType, + I: Iterator + ExactSizeIterator, + T: GraphQLType + ?Sized + 't, { let stop_on_null = executor .current_type() @@ -195,30 +242,26 @@ where let mut result = Vec::with_capacity(iter.len()); for o in iter { - match executor.resolve(info, &o) { - Ok(value) => { - if stop_on_null && value.is_null() { - return Ok(value); - } else { - result.push(value) - } - } - Err(e) => return Err(e), + let val = executor.resolve(info, o)?; + if stop_on_null && val.is_null() { + return Ok(val); + } else { + result.push(val) } } Ok(Value::list(result)) } -async fn resolve_into_list_async<'a, S, T, I>( +async fn resolve_into_list_async<'a, 't, S, T, I>( executor: &'a Executor<'a, 'a, T::Context, S>, info: &'a T::TypeInfo, items: I, ) -> ExecutionResult where S: ScalarValue + Send + Sync, - I: Iterator + ExactSizeIterator, - T: crate::GraphQLTypeAsync, + I: Iterator + ExactSizeIterator, + T: GraphQLTypeAsync + ?Sized + 't, T::TypeInfo: Send + Sync, T::Context: Send + Sync, { @@ -231,8 +274,7 @@ where .expect("Current type is not a list type") .is_non_null(); - let iter = - items.map(|item| async move { executor.resolve_into_value_async(info, &item).await }); + let iter = items.map(|item| async move { executor.resolve_into_value_async(info, item).await }); let mut futures = FuturesOrdered::from_iter(iter); let mut values = Vec::with_capacity(futures.len()); @@ -245,63 +287,3 @@ where Ok(Value::list(values)) } - -impl crate::GraphQLTypeAsync for Vec -where - T: crate::GraphQLTypeAsync, - T::TypeInfo: Send + Sync, - S: ScalarValue + Send + Sync, - CtxT: Send + Sync, -{ - fn resolve_async<'a>( - &'a self, - info: &'a Self::TypeInfo, - _selection_set: Option<&'a [Selection]>, - executor: &'a Executor, - ) -> crate::BoxFuture<'a, ExecutionResult> { - let f = resolve_into_list_async(executor, info, self.iter()); - Box::pin(f) - } -} - -impl crate::GraphQLTypeAsync for &[T] -where - T: crate::GraphQLTypeAsync, - T::TypeInfo: Send + Sync, - S: ScalarValue + Send + Sync, - CtxT: Send + Sync, -{ - fn resolve_async<'a>( - &'a self, - info: &'a Self::TypeInfo, - _selection_set: Option<&'a [Selection]>, - executor: &'a Executor, - ) -> crate::BoxFuture<'a, ExecutionResult> { - let f = resolve_into_list_async(executor, info, self.iter()); - Box::pin(f) - } -} - -impl crate::GraphQLTypeAsync for Option -where - T: crate::GraphQLTypeAsync, - T::TypeInfo: Send + Sync, - S: ScalarValue + Send + Sync, - CtxT: Send + Sync, -{ - fn resolve_async<'a>( - &'a self, - info: &'a Self::TypeInfo, - _selection_set: Option<&'a [Selection]>, - executor: &'a Executor, - ) -> crate::BoxFuture<'a, ExecutionResult> { - let f = async move { - let value = match *self { - Some(ref obj) => executor.resolve_into_value_async(info, obj).await, - None => Value::null(), - }; - Ok(value) - }; - Box::pin(f) - } -} diff --git a/juniper/src/types/marker.rs b/juniper/src/types/marker.rs index e3d42432..5e2fc909 100644 --- a/juniper/src/types/marker.rs +++ b/juniper/src/types/marker.rs @@ -23,6 +23,30 @@ pub trait GraphQLObjectType: GraphQLType { 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. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +/// [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-Interfaces +pub trait GraphQLUnion: 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() {} +} + /// Marker trait for types which can be used as output types. /// /// The GraphQL specification differentiates between input and output diff --git a/juniper/src/types/pointers.rs b/juniper/src/types/pointers.rs index b599b2ca..68274fad 100644 --- a/juniper/src/types/pointers.rs +++ b/juniper/src/types/pointers.rs @@ -1,17 +1,21 @@ -use crate::ast::{FromInputValue, InputValue, Selection, ToInputValue}; use std::{fmt::Debug, sync::Arc}; use crate::{ + ast::{FromInputValue, InputValue, Selection, ToInputValue}, executor::{ExecutionResult, Executor, Registry}, schema::meta::MetaType, - types::base::{Arguments, GraphQLType}, + types::{ + async_await::GraphQLTypeAsync, + base::{Arguments, GraphQLType}, + }, value::ScalarValue, + BoxFuture, }; impl GraphQLType for Box where S: ScalarValue, - T: GraphQLType, + T: GraphQLType + ?Sized, { type Context = CtxT; type TypeInfo = T::TypeInfo; @@ -57,6 +61,23 @@ where } } +impl crate::GraphQLTypeAsync for Box +where + T: GraphQLTypeAsync + ?Sized, + T::TypeInfo: Send + Sync, + S: ScalarValue + Send + Sync, + CtxT: Send + Sync, +{ + fn resolve_async<'a>( + &'a self, + info: &'a Self::TypeInfo, + selection_set: Option<&'a [Selection]>, + executor: &'a Executor, + ) -> BoxFuture<'a, ExecutionResult> { + (**self).resolve_async(info, selection_set, executor) + } +} + impl FromInputValue for Box where S: ScalarValue, @@ -83,7 +104,7 @@ where impl<'e, S, T, CtxT> GraphQLType for &'e T where S: ScalarValue, - T: GraphQLType, + T: GraphQLType + ?Sized, { type Context = CtxT; type TypeInfo = T::TypeInfo; @@ -129,10 +150,10 @@ where } } -impl<'e, S, T> crate::GraphQLTypeAsync for &'e T +impl<'e, S, T> GraphQLTypeAsync for &'e T where S: ScalarValue + Send + Sync, - T: crate::GraphQLTypeAsync, + T: GraphQLTypeAsync + ?Sized, T::TypeInfo: Send + Sync, T::Context: Send + Sync, { @@ -142,8 +163,8 @@ where field_name: &'b str, arguments: &'b Arguments, executor: &'b Executor, - ) -> crate::BoxFuture<'b, ExecutionResult> { - crate::GraphQLTypeAsync::resolve_field_async(&**self, info, field_name, arguments, executor) + ) -> BoxFuture<'b, ExecutionResult> { + GraphQLTypeAsync::resolve_field_async(&**self, info, field_name, arguments, executor) } fn resolve_async<'a>( @@ -151,8 +172,8 @@ where info: &'a Self::TypeInfo, selection_set: Option<&'a [Selection]>, executor: &'a Executor, - ) -> crate::BoxFuture<'a, ExecutionResult> { - crate::GraphQLTypeAsync::resolve_async(&**self, info, selection_set, executor) + ) -> BoxFuture<'a, ExecutionResult> { + GraphQLTypeAsync::resolve_async(&**self, info, selection_set, executor) } } @@ -169,7 +190,7 @@ where impl GraphQLType for Arc where S: ScalarValue, - T: GraphQLType, + T: GraphQLType + ?Sized, { type Context = T::Context; type TypeInfo = T::TypeInfo; @@ -215,36 +236,19 @@ where } } -impl crate::GraphQLTypeAsync for Box +impl<'e, S, T> GraphQLTypeAsync for Arc where - T: crate::GraphQLTypeAsync, - T::TypeInfo: Send + Sync, S: ScalarValue + Send + Sync, - CtxT: Send + Sync, + T: GraphQLTypeAsync + ?Sized, + >::TypeInfo: Send + Sync, + >::Context: Send + Sync, { fn resolve_async<'a>( &'a self, info: &'a Self::TypeInfo, selection_set: Option<&'a [Selection]>, executor: &'a Executor, - ) -> crate::BoxFuture<'a, crate::ExecutionResult> { - (**self).resolve_async(info, selection_set, executor) - } -} - -impl<'e, S, T> crate::GraphQLTypeAsync for std::sync::Arc -where - S: ScalarValue + Send + Sync, - T: crate::GraphQLTypeAsync, - >::TypeInfo: Send + Sync, - >::Context: Send + Sync, -{ - fn resolve_async<'a>( - &'a self, - info: &'a Self::TypeInfo, - selection_set: Option<&'a [Selection]>, - executor: &'a Executor, - ) -> crate::BoxFuture<'a, crate::ExecutionResult> { + ) -> BoxFuture<'a, ExecutionResult> { (**self).resolve_async(info, selection_set, executor) } } diff --git a/juniper/src/types/scalars.rs b/juniper/src/types/scalars.rs index 10d3d7f4..a2963c93 100644 --- a/juniper/src/types/scalars.rs +++ b/juniper/src/types/scalars.rs @@ -192,7 +192,7 @@ where }) } -impl<'a, S> GraphQLType for &'a str +impl GraphQLType for str where S: ScalarValue, { @@ -216,11 +216,11 @@ where _: Option<&[Selection]>, _: &Executor, ) -> ExecutionResult { - Ok(Value::scalar(String::from(*self))) + Ok(Value::scalar(String::from(self))) } } -impl<'e, S> crate::GraphQLTypeAsync for &'e str +impl crate::GraphQLTypeAsync for str where S: ScalarValue + Send + Sync, { @@ -325,11 +325,11 @@ where /// If you instantiate `RootNode` with this as the mutation, no mutation will be /// generated for the schema. #[derive(Debug)] -pub struct EmptyMutation { +pub struct EmptyMutation { phantom: PhantomData, } -impl EmptyMutation { +impl EmptyMutation { /// Construct a new empty mutation pub fn new() -> EmptyMutation { EmptyMutation { @@ -339,7 +339,7 @@ impl EmptyMutation { } // This is safe because `T` is never used. -unsafe impl Send for EmptyMutation {} +unsafe impl Send for EmptyMutation {} impl GraphQLType for EmptyMutation where @@ -382,14 +382,14 @@ impl Default for EmptyMutation { /// /// If you instantiate `RootNode` with this as the subscription, /// no subscriptions will be generated for the schema. -pub struct EmptySubscription { +pub struct EmptySubscription { phantom: PhantomData, } // This is safe due to never using `T`. -unsafe impl Send for EmptySubscription {} +unsafe impl Send for EmptySubscription {} -impl EmptySubscription { +impl EmptySubscription { /// Construct a new empty subscription pub fn new() -> Self { EmptySubscription { diff --git a/juniper/src/types/subscriptions.rs b/juniper/src/types/subscriptions.rs index 5b6ccc56..050769cd 100644 --- a/juniper/src/types/subscriptions.rs +++ b/juniper/src/types/subscriptions.rs @@ -184,7 +184,7 @@ where 'e: 'fut, 'ref_e: 'fut, 'res: 'fut, - T: GraphQLSubscriptionType, + T: GraphQLSubscriptionType + ?Sized, T::TypeInfo: Send + Sync, S: ScalarValue + Send + Sync, CtxT: Send + Sync, @@ -203,7 +203,7 @@ async fn resolve_selection_set_into_stream_recursive<'i, 'inf, 'ref_e, 'e, 'res, executor: &'ref_e Executor<'ref_e, 'e, CtxT, S>, ) -> Value> where - T: GraphQLSubscriptionType + Send + Sync, + T: GraphQLSubscriptionType + Send + Sync + ?Sized, T::TypeInfo: Send + Sync, S: ScalarValue + Send + Sync, CtxT: Send + Sync, diff --git a/juniper_codegen/Cargo.toml b/juniper_codegen/Cargo.toml index be8639cd..21faf7cc 100644 --- a/juniper_codegen/Cargo.toml +++ b/juniper_codegen/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "juniper_codegen" version = "0.14.2" +edition = "2018" authors = [ "Magnus Hallin ", "Christoph Herzog ", @@ -9,20 +10,20 @@ description = "Internal custom derive trait for Juniper GraphQL" license = "BSD-2-Clause" documentation = "https://docs.rs/juniper" repository = "https://github.com/graphql-rust/juniper" -edition = "2018" + +[badges] +travis-ci = { repository = "graphql-rust/juniper" } [lib] proc-macro = true [dependencies] -proc-macro2 = "1.0.1" -syn = { version = "1.0.3", features = ["full", "extra-traits", "parsing"] } -quote = "1.0.3" -futures = "0.3.1" proc-macro-error = "1.0.2" +proc-macro2 = "1.0.1" +quote = "1.0.3" +syn = { version = "1.0.3", features = ["full", "extra-traits", "parsing"] } [dev-dependencies] -juniper = { version = "0.14.2", path = "../juniper"} - -[badges] -travis-ci = { repository = "graphql-rust/juniper" } +derive_more = "0.99.7" +futures = "0.3.1" +juniper = { version = "0.14.2", path = "../juniper" } diff --git a/juniper_codegen/src/derive_enum.rs b/juniper_codegen/src/derive_enum.rs index 9dc9b7dc..b1a7e4cd 100644 --- a/juniper_codegen/src/derive_enum.rs +++ b/juniper_codegen/src/derive_enum.rs @@ -58,7 +58,7 @@ pub fn impl_enum( let _type = match field.fields { Fields::Unit => syn::parse_str(&field_name.to_string()).unwrap(), _ => { - error.custom( + error.emit_custom( field.fields.span(), "all fields of the enum must be unnamed, e.g., None", ); @@ -145,6 +145,7 @@ pub fn impl_enum( include_type_generics: true, generic_scalar: true, no_async: attrs.no_async.is_some(), + mode: is_internal.into(), }; let juniper_crate_name = if is_internal { "crate" } else { "juniper" }; diff --git a/juniper_codegen/src/derive_input_object.rs b/juniper_codegen/src/derive_input_object.rs index 843bfb2d..a85cd4d8 100644 --- a/juniper_codegen/src/derive_input_object.rs +++ b/juniper_codegen/src/derive_input_object.rs @@ -145,6 +145,7 @@ pub fn impl_input_object( include_type_generics: true, generic_scalar: true, no_async: attrs.no_async.is_some(), + mode: is_internal.into(), }; let juniper_crate_name = if is_internal { "crate" } else { "juniper" }; diff --git a/juniper_codegen/src/derive_object.rs b/juniper_codegen/src/derive_object.rs index 04dc4556..ad5b7cc7 100644 --- a/juniper_codegen/src/derive_object.rs +++ b/juniper_codegen/src/derive_object.rs @@ -132,6 +132,7 @@ pub fn build_derive_object( include_type_generics: true, generic_scalar: true, no_async: attrs.no_async.is_some(), + mode: is_internal.into(), }; let juniper_crate_name = if is_internal { "crate" } else { "juniper" }; diff --git a/juniper_codegen/src/derive_scalar_value.rs b/juniper_codegen/src/derive_scalar_value.rs index 6474324b..c951b1e5 100644 --- a/juniper_codegen/src/derive_scalar_value.rs +++ b/juniper_codegen/src/derive_scalar_value.rs @@ -127,7 +127,7 @@ fn impl_scalar_struct( executor: &'a #crate_name::Executor, ) -> #crate_name::BoxFuture<'a, #crate_name::ExecutionResult<__S>> { use #crate_name::GraphQLType; - use futures::future; + use #crate_name::futures::future; let v = self.resolve(info, selection_set, executor); Box::pin(future::ready(v)) } diff --git a/juniper_codegen/src/derive_union.rs b/juniper_codegen/src/derive_union.rs deleted file mode 100644 index 0b94304e..00000000 --- a/juniper_codegen/src/derive_union.rs +++ /dev/null @@ -1,182 +0,0 @@ -use crate::{ - result::{GraphQLScope, UnsupportedAttribute}, - util::{self, span_container::SpanContainer}, -}; -use proc_macro2::TokenStream; -use quote::quote; -use syn::{self, ext::IdentExt, spanned::Spanned, Data, Fields}; - -pub fn build_derive_union( - ast: syn::DeriveInput, - is_internal: bool, - error: GraphQLScope, -) -> syn::Result { - let ast_span = ast.span(); - let enum_fields = match ast.data { - Data::Enum(data) => data.variants, - _ => return Err(error.custom_error(ast_span, "can only be applied to enums")), - }; - - // Parse attributes. - let attrs = util::ObjectAttributes::from_attrs(&ast.attrs)?; - - let ident = &ast.ident; - let name = attrs - .name - .clone() - .map(SpanContainer::into_inner) - .unwrap_or_else(|| ident.unraw().to_string()); - - let fields = enum_fields - .into_iter() - .filter_map(|field| { - let span = field.span(); - let field_attrs = match util::FieldAttributes::from_attrs( - &field.attrs, - util::FieldAttributeParseMode::Object, - ) { - Ok(attrs) => attrs, - Err(e) => { - proc_macro_error::emit_error!(e); - return None; - } - }; - - if let Some(ident) = field_attrs.skip { - error.unsupported_attribute_within(ident.span(), UnsupportedAttribute::Skip); - return None; - } - - let variant_name = field.ident; - let name = field_attrs - .name - .clone() - .map(SpanContainer::into_inner) - .unwrap_or_else(|| util::to_camel_case(&variant_name.unraw().to_string())); - - let resolver_code = quote!( - #ident :: #variant_name - ); - - let _type = match field.fields { - Fields::Unnamed(inner) => { - let mut iter = inner.unnamed.iter(); - let first = match iter.next() { - Some(val) => val, - None => unreachable!(), - }; - - if iter.next().is_some() { - error.custom( - inner.span(), - "all members must be unnamed with a single element e.g. Some(T)", - ); - } - - first.ty.clone() - } - _ => { - error.custom( - variant_name.span(), - "only unnamed fields with a single element are allowed, e.g., Some(T)", - ); - - return None; - } - }; - - if let Some(description) = field_attrs.description { - error.unsupported_attribute_within( - description.span_ident(), - UnsupportedAttribute::Description, - ); - } - - if let Some(default) = field_attrs.default { - error.unsupported_attribute_within( - default.span_ident(), - UnsupportedAttribute::Default, - ); - } - - if name.starts_with("__") { - error.no_double_underscore(if let Some(name) = field_attrs.name { - name.span_ident() - } else { - variant_name.span() - }); - } - - Some(util::GraphQLTypeDefinitionField { - name, - _type, - args: Vec::new(), - description: None, - deprecation: field_attrs.deprecation.map(SpanContainer::into_inner), - resolver_code, - is_type_inferred: true, - is_async: false, - default: None, - span, - }) - }) - .collect::>(); - - // 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 fields.is_empty() { - error.not_empty(ast_span); - } - - if name.starts_with("__") && !is_internal { - error.no_double_underscore(if let Some(name) = attrs.name { - name.span_ident() - } else { - ident.span() - }); - } - - // NOTICE: This is not an optimal implementation. It is possible - // to bypass this check by using a full qualified path instead - // (crate::Test vs Test). Since this requirement is mandatory, the - // `std::convert::Into` implementation is used to enforce this - // requirement. However, due to the bad error message this - // implementation should stay and provide guidance. - let all_variants_different = { - let mut all_types: Vec<_> = fields.iter().map(|field| &field._type).collect(); - let before = all_types.len(); - all_types.dedup(); - before == all_types.len() - }; - - if !all_variants_different { - error.custom(ident.span(), "each variant must have a different type"); - } - - // Early abort after GraphQL properties - proc_macro_error::abort_if_dirty(); - - let definition = util::GraphQLTypeDefiniton { - name, - _type: syn::parse_str(&ast.ident.to_string()).unwrap(), - context: attrs.context.map(SpanContainer::into_inner), - scalar: attrs.scalar.map(SpanContainer::into_inner), - description: attrs.description.map(SpanContainer::into_inner), - fields, - generics: ast.generics, - interfaces: None, - include_type_generics: true, - generic_scalar: true, - no_async: attrs.no_async.is_some(), - }; - - let juniper_crate_name = if is_internal { "crate" } else { "juniper" }; - Ok(definition.into_union_tokens(juniper_crate_name)) -} diff --git a/juniper_codegen/src/graphql_union/attr.rs b/juniper_codegen/src/graphql_union/attr.rs new file mode 100644 index 00000000..d8561f69 --- /dev/null +++ b/juniper_codegen/src/graphql_union/attr.rs @@ -0,0 +1,345 @@ +//! Code generation for `#[graphql_union]`/`#[graphql_union_internal]` macros. + +use std::{mem, ops::Deref as _}; + +use proc_macro2::{Span, TokenStream}; +use quote::{quote, ToTokens as _}; +use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned as _}; + +use crate::{ + result::GraphQLScope, + util::{path_eq_single, span_container::SpanContainer, unparenthesize, Mode}, +}; + +use super::{ + all_variants_different, emerge_union_variants_from_meta, UnionDefinition, UnionMeta, + UnionVariantDefinition, UnionVariantMeta, +}; + +/// [`GraphQLScope`] of errors for `#[graphql_union]`/`#[graphql_union_internal]` macros. +const ERR: GraphQLScope = GraphQLScope::UnionAttr; + +/// Returns the concrete name of the `proc_macro_attribute` for deriving `GraphQLUnion` +/// implementation depending on the provided `mode`. +fn attr_path(mode: Mode) -> &'static str { + match mode { + Mode::Public => "graphql_union", + Mode::Internal => "graphql_union_internal", + } +} + +/// Expands `#[graphql_union]`/`#[graphql_union_internal]` macro into generated code. +pub fn expand(attr_args: TokenStream, body: TokenStream, mode: Mode) -> syn::Result { + let attr_path = attr_path(mode); + + let mut ast = syn::parse2::(body).map_err(|_| { + syn::Error::new( + Span::call_site(), + format!( + "#[{}] attribute is applicable to trait definitions only", + attr_path, + ), + ) + })?; + + let mut trait_attrs = Vec::with_capacity(ast.attrs.len() + 1); + trait_attrs.push({ + let attr_path = syn::Ident::new(attr_path, Span::call_site()); + parse_quote! { #[#attr_path(#attr_args)] } + }); + trait_attrs.extend_from_slice(&ast.attrs); + + // Remove repeated attributes from the definition, to omit duplicate expansion. + ast.attrs = ast + .attrs + .into_iter() + .filter_map(|attr| { + if path_eq_single(&attr.path, attr_path) { + None + } else { + Some(attr) + } + }) + .collect(); + + let meta = UnionMeta::from_attrs(attr_path, &trait_attrs)?; + + let trait_span = ast.span(); + let trait_ident = &ast.ident; + + let name = meta + .name + .clone() + .map(SpanContainer::into_inner) + .unwrap_or_else(|| trait_ident.unraw().to_string()); + if matches!(mode, Mode::Public) && name.starts_with("__") { + ERR.no_double_underscore( + meta.name + .as_ref() + .map(SpanContainer::span_ident) + .unwrap_or_else(|| trait_ident.span()), + ); + } + + let mut variants: Vec<_> = ast + .items + .iter_mut() + .filter_map(|i| match i { + syn::TraitItem::Method(m) => { + parse_variant_from_trait_method(m, trait_ident, &meta, mode) + } + _ => None, + }) + .collect(); + + proc_macro_error::abort_if_dirty(); + + emerge_union_variants_from_meta(&mut variants, meta.external_resolvers, mode); + + if variants.is_empty() { + ERR.emit_custom(trait_span, "expects at least one union variant"); + } + + if !all_variants_different(&variants) { + ERR.emit_custom( + trait_span, + "must have a different type for each union variant", + ); + } + + proc_macro_error::abort_if_dirty(); + + let context = meta + .context + .map(SpanContainer::into_inner) + .or_else(|| variants.iter().find_map(|v| v.context_ty.as_ref()).cloned()); + + let generated_code = UnionDefinition { + name, + ty: parse_quote! { #trait_ident }, + is_trait_object: true, + description: meta.description.map(SpanContainer::into_inner), + context, + scalar: meta.scalar.map(SpanContainer::into_inner), + generics: ast.generics.clone(), + variants, + span: trait_span, + mode, + }; + + Ok(quote! { + #ast + + #generated_code + }) +} + +/// Parses given Rust trait `method` as [GraphQL union][1] variant. +/// +/// On failure returns [`None`] and internally fills up [`proc_macro_error`] with the corresponding +/// errors. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +fn parse_variant_from_trait_method( + method: &mut syn::TraitItemMethod, + trait_ident: &syn::Ident, + trait_meta: &UnionMeta, + mode: Mode, +) -> Option { + let attr_path = attr_path(mode); + 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_map(|attr| { + if path_eq_single(&attr.path, attr_path) { + None + } else { + Some(attr) + } + }) + .collect(); + + let meta = UnionVariantMeta::from_attrs(attr_path, &method_attrs) + .map_err(|e| proc_macro_error::emit_error!(e)) + .ok()?; + + if let Some(rslvr) = meta.external_resolver { + ERR.custom( + rslvr.span_ident(), + format!( + "cannot use #[{}(with = ...)] attribute on a trait method", + attr_path, + ), + ) + .note(format!( + "instead use #[{0}(ignore)] on the method with #[{0}(on ... = ...)] on the trait \ + itself", + attr_path, + )) + .emit() + } + if meta.ignore.is_some() { + return None; + } + + let method_span = method.sig.span(); + let method_ident = &method.sig.ident; + + let ty = parse_trait_method_output_type(&method.sig) + .map_err(|span| { + ERR.emit_custom( + span, + "expects trait method return type to be `Option<&VariantType>` only", + ) + }) + .ok()?; + let method_context_ty = parse_trait_method_input_args(&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(), + "doesn't support async union variants resolvers yet", + ); + return None; + } + + let resolver_code = { + if let Some(other) = trait_meta.external_resolvers.get(&ty) { + ERR.custom( + method_span, + format!( + "trait method `{}` conflicts with the external resolver function `{}` declared \ + on the trait to resolve the variant type `{}`", + method_ident, + other.to_token_stream(), + ty.to_token_stream(), + + ), + ) + .note(format!( + "use `#[{}(ignore)]` attribute to ignore this trait method for union variants \ + resolution", + attr_path, + )) + .emit(); + } + + if method_context_ty.is_some() { + let crate_path = mode.crate_path(); + + parse_quote! { + #trait_ident::#method_ident(self, #crate_path::FromContext::from(context)) + } + } else { + parse_quote! { + #trait_ident::#method_ident(self) + } + } + }; + + // Doing this may be quite an expensive, because resolving may contain some heavy computation, + // so we're preforming it twice. Unfortunately, we have no other options here, until the + // `juniper::GraphQLType` itself will allow to do it in some cleverer way. + let resolver_check = parse_quote! { + ({ #resolver_code } as ::std::option::Option<&#ty>).is_some() + }; + + Some(UnionVariantDefinition { + ty, + resolver_code, + resolver_check, + enum_path: None, + context_ty: method_context_ty, + span: method_span, + }) +} + +/// Parses type of [GraphQL union][1] variant from the return type of trait method. +/// +/// If return type is invalid, then returns the [`Span`] to display the corresponding error at. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +fn parse_trait_method_output_type(sig: &syn::Signature) -> 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().skip(1).next() { + 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 new file mode 100644 index 00000000..8f0f7ed5 --- /dev/null +++ b/juniper_codegen/src/graphql_union/derive.rs @@ -0,0 +1,226 @@ +//! Code generation for `#[derive(GraphQLUnion)]`/`#[derive(GraphQLUnionInternal)]` macros. + +use proc_macro2::TokenStream; +use proc_macro_error::ResultExt as _; +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, Mode}, +}; + +use super::{ + all_variants_different, emerge_union_variants_from_meta, UnionDefinition, UnionMeta, + UnionVariantDefinition, UnionVariantMeta, +}; + +/// [`GraphQLScope`] of errors for `#[derive(GraphQLUnion)]`/`#[derive(GraphQLUnionInternal)]` +/// macros. +const ERR: GraphQLScope = GraphQLScope::UnionDerive; + +/// Expands `#[derive(GraphQLUnion)]`/`#[derive(GraphQLUnionInternal)]` macro into generated code. +pub fn expand(input: TokenStream, mode: Mode) -> syn::Result { + let ast = syn::parse2::(input).unwrap_or_abort(); + + match &ast.data { + Data::Enum(_) => expand_enum(ast, mode), + Data::Struct(_) => expand_struct(ast, mode), + _ => Err(ERR.custom_error(ast.span(), "can only be derived for enums and structs")), + } + .map(ToTokens::into_token_stream) +} + +/// Expands into generated code a `#[derive(GraphQLUnion)]`/`#[derive(GraphQLUnionInternal)]` macro +/// placed on a Rust enum. +fn expand_enum(ast: syn::DeriveInput, mode: Mode) -> syn::Result { + let meta = UnionMeta::from_attrs("graphql", &ast.attrs)?; + + let enum_span = ast.span(); + let enum_ident = ast.ident; + + let name = meta + .name + .clone() + .map(SpanContainer::into_inner) + .unwrap_or_else(|| enum_ident.unraw().to_string()); + if matches!(mode, Mode::Public) && name.starts_with("__") { + ERR.no_double_underscore( + meta.name + .as_ref() + .map(SpanContainer::span_ident) + .unwrap_or_else(|| enum_ident.span()), + ); + } + + let mut variants: Vec<_> = match ast.data { + Data::Enum(data) => data.variants, + _ => unreachable!(), + } + .into_iter() + .filter_map(|var| parse_variant_from_enum_variant(var, &enum_ident, &meta, mode)) + .collect(); + + proc_macro_error::abort_if_dirty(); + + emerge_union_variants_from_meta(&mut variants, meta.external_resolvers, mode); + + if variants.is_empty() { + ERR.emit_custom(enum_span, "expects at least one union variant"); + } + + if !all_variants_different(&variants) { + ERR.emit_custom( + enum_span, + "must have a different type for each union variant", + ); + } + + proc_macro_error::abort_if_dirty(); + + Ok(UnionDefinition { + name, + ty: parse_quote! { #enum_ident }, + is_trait_object: false, + description: meta.description.map(SpanContainer::into_inner), + context: meta.context.map(SpanContainer::into_inner), + scalar: meta.scalar.map(SpanContainer::into_inner), + generics: ast.generics, + variants, + span: enum_span, + mode, + }) +} + +/// Parses given Rust enum `var`iant as [GraphQL union][1] variant. +/// +/// On failure returns [`None`] and internally fills up [`proc_macro_error`] with the corresponding +/// errors. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +fn parse_variant_from_enum_variant( + var: syn::Variant, + enum_ident: &syn::Ident, + enum_meta: &UnionMeta, + mode: Mode, +) -> Option { + let meta = UnionVariantMeta::from_attrs("graphql", &var.attrs) + .map_err(|e| proc_macro_error::emit_error!(e)) + .ok()?; + if meta.ignore.is_some() { + return None; + } + + let var_span = var.span(); + let var_ident = var.ident; + + let ty = match var.fields { + Fields::Unnamed(fields) => { + let mut iter = fields.unnamed.iter(); + let first = iter.next().unwrap(); + if iter.next().is_none() { + Ok(unparenthesize(&first.ty).clone()) + } else { + Err(fields.span()) + } + } + _ => Err(var_ident.span()), + } + .map_err(|span| { + ERR.emit_custom( + span, + "enum allows only unnamed variants with a single field, e.g. `Some(T)`", + ) + }) + .ok()?; + + let enum_path = quote! { #enum_ident::#var_ident }; + + let resolver_code = if let Some(rslvr) = meta.external_resolver { + if let Some(other) = enum_meta.external_resolvers.get(&ty) { + ERR.emit_custom( + rslvr.span_ident(), + format!( + "variant `{}` already has external resolver function `{}` declared on the enum", + ty.to_token_stream(), + other.to_token_stream(), + ), + ); + } + + let crate_path = mode.crate_path(); + let resolver_fn = rslvr.into_inner(); + + parse_quote! { + #resolver_fn(self, #crate_path::FromContext::from(context)) + } + } else { + parse_quote! { + match self { #enum_ident::#var_ident(ref v) => Some(v), _ => None, } + } + }; + + let resolver_check = parse_quote! { + matches!(self, #enum_path(_)) + }; + + Some(UnionVariantDefinition { + ty, + resolver_code, + resolver_check, + enum_path: Some(enum_path), + context_ty: None, + span: var_span, + }) +} + +/// Expands into generated code a `#[derive(GraphQLUnion)]`/`#[derive(GraphQLUnionInternal)]` macro +/// placed on a Rust struct. +fn expand_struct(ast: syn::DeriveInput, mode: Mode) -> syn::Result { + let meta = UnionMeta::from_attrs("graphql", &ast.attrs)?; + + let struct_span = ast.span(); + let struct_ident = ast.ident; + + let name = meta + .name + .clone() + .map(SpanContainer::into_inner) + .unwrap_or_else(|| struct_ident.unraw().to_string()); + if matches!(mode, Mode::Public) && name.starts_with("__") { + ERR.no_double_underscore( + meta.name + .as_ref() + .map(SpanContainer::span_ident) + .unwrap_or_else(|| struct_ident.span()), + ); + } + + let mut variants = vec![]; + emerge_union_variants_from_meta(&mut variants, meta.external_resolvers, mode); + if variants.is_empty() { + ERR.emit_custom(struct_span, "expects at least one union variant"); + } + + if !all_variants_different(&variants) { + ERR.emit_custom( + struct_span, + "must have a different type for each union variant", + ); + } + + proc_macro_error::abort_if_dirty(); + + Ok(UnionDefinition { + name, + ty: parse_quote! { #struct_ident }, + is_trait_object: false, + description: meta.description.map(SpanContainer::into_inner), + context: meta.context.map(SpanContainer::into_inner), + scalar: meta.scalar.map(SpanContainer::into_inner), + generics: ast.generics, + variants, + span: struct_span, + mode, + }) +} diff --git a/juniper_codegen/src/graphql_union/mod.rs b/juniper_codegen/src/graphql_union/mod.rs new file mode 100644 index 00000000..2d8132c2 --- /dev/null +++ b/juniper_codegen/src/graphql_union/mod.rs @@ -0,0 +1,700 @@ +//! Code generation for [GraphQL union][1]. +//! +//! [1]: https://spec.graphql.org/June2018/#sec-Unions + +pub mod attr; +pub mod derive; + +use std::collections::HashMap; + +use proc_macro2::{Span, TokenStream}; +use quote::{quote, ToTokens, TokenStreamExt as _}; +use syn::{ + parse::{Parse, ParseStream}, + parse_quote, + spanned::Spanned as _, +}; + +use crate::util::{ + filter_attrs, get_doc_comment, span_container::SpanContainer, Mode, 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") +} + +/// Helper alias for the type of [`UnionMeta::external_resolvers`] field. +type UnionMetaResolvers = HashMap>; + +/// Available metadata (arguments) behind `#[graphql]` (or `#[graphql_union]`) attribute when +/// generating code for [GraphQL union][1] type. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +#[derive(Debug, Default)] +struct UnionMeta { + /// Explicitly specified name of [GraphQL union][1] type. + /// + /// If absent, then Rust type name is used by default. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub name: Option>, + + /// Explicitly specified [description][2] of [GraphQL union][1] type. + /// + /// If absent, then Rust doc comment is used as [description][2], if any. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + /// [2]: https://spec.graphql.org/June2018/#sec-Descriptions + pub description: Option>, + + /// Explicitly specified type of `juniper::Context` to use for resolving this [GraphQL union][1] + /// type with. + /// + /// If absent, then unit type `()` is assumed as type of `juniper::Context`. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub context: Option>, + + /// Explicitly specified type of `juniper::ScalarValue` to use for resolving this + /// [GraphQL union][1] type with. + /// + /// If absent, then generated code will be generic over any `juniper::ScalarValue` type, which, + /// in turn, requires all [union][1] variants to be generic over any `juniper::ScalarValue` type + /// too. That's why this type should be specified only if one of the variants implements + /// `juniper::GraphQLType` in a non-generic way over `juniper::ScalarValue` type. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub scalar: Option>, + + /// Explicitly specified external resolver functions for [GraphQL union][1] variants. + /// + /// If absent, then macro will try to auto-infer all the possible variants from the type + /// declaration, if possible. That's why specifying an external resolver function has sense, + /// when some custom [union][1] variant resolving logic is involved, or variants cannot be + /// inferred. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub external_resolvers: UnionMetaResolvers, +} + +impl Parse for UnionMeta { + fn parse(input: ParseStream) -> syn::Result { + let mut output = Self::default(); + + while !input.is_empty() { + let ident: syn::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.value(), + )) + .none_or_else(|_| dup_attr_err(ident.span()))? + } + "desc" | "description" => { + input.parse::()?; + let desc = input.parse::()?; + output + .description + .replace(SpanContainer::new( + ident.span(), + Some(desc.span()), + desc.value(), + )) + .none_or_else(|_| dup_attr_err(ident.span()))? + } + "ctx" | "context" | "Context" => { + 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()))? + } + "scalar" | "Scalar" | "ScalarValue" => { + 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()))? + } + "on" => { + let ty = 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))? + } + _ => { + return Err(syn::Error::new(ident.span(), "unknown attribute")); + } + } + if input.lookahead1().peek(syn::Token![,]) { + input.parse::()?; + } + } + + Ok(output) + } +} + +impl UnionMeta { + /// Tries to merge two [`UnionMeta`]s into 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), + }) + } + + /// Parses [`UnionMeta`] from the given multiple `name`d attributes placed on 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()) + .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?; + + if meta.description.is_none() { + meta.description = get_doc_comment(attrs); + } + + Ok(meta) + } +} + +/// Available metadata (arguments) behind `#[graphql]` (or `#[graphql_union]`) attribute when +/// generating code for [GraphQL union][1]'s variant. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +#[derive(Debug, Default)] +struct UnionVariantMeta { + /// Explicitly specified marker for the variant/field being ignored and not included into + /// [GraphQL union][1]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub ignore: Option>, + + /// Explicitly specified external resolver function for this [GraphQL union][1] variant. + /// + /// If absent, then macro will generate the code which just returns the variant inner value. + /// Usually, specifying an external resolver function has sense, when some custom resolving + /// logic is involved. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub external_resolver: Option>, +} + +impl Parse for UnionVariantMeta { + fn parse(input: ParseStream) -> syn::Result { + let mut output = Self::default(); + + while !input.is_empty() { + let ident: syn::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()))?, + "with" => { + 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()))? + } + _ => { + return Err(syn::Error::new(ident.span(), "unknown attribute")); + } + } + if input.lookahead1().peek(syn::Token![,]) { + input.parse::()?; + } + } + + Ok(output) + } +} + +impl UnionVariantMeta { + /// Tries to merge two [`UnionVariantMeta`]s into single one, reporting about duplicates, if + /// any. + fn try_merge(self, mut another: Self) -> syn::Result { + Ok(Self { + ignore: try_merge_opt!(ignore: self, another), + external_resolver: try_merge_opt!(external_resolver: self, another), + }) + } + + /// Parses [`UnionVariantMeta`] from the given multiple `name`d attributes placed on + /// variant/field/method definition. + 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?)) + } +} + +/// Definition of [GraphQL union][1] variant for code generation. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +struct UnionVariantDefinition { + /// Rust type that this [GraphQL union][1] variant resolves into. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub ty: syn::Type, + + /// Rust code for value resolution of this [GraphQL union][1] variant. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub resolver_code: syn::Expr, + + /// Rust code for checking whether [GraphQL union][1] should be resolved into this variant. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub resolver_check: syn::Expr, + + /// Rust enum variant path that this [GraphQL union][1] variant is associated with. + /// + /// It's available only when code generation happens for Rust enums. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub enum_path: Option, + + /// Rust type of `juniper::Context` that this [GraphQL union][1] variant requires for + /// resolution. + /// + /// It's available only when code generation happens for Rust traits and a trait method contains + /// context argument. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub context_ty: Option, + + /// [`Span`] that points to the Rust source code which defines this [GraphQL union][1] variant. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub span: Span, +} + +/// Definition of [GraphQL union][1] for code generation. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +struct UnionDefinition { + /// Name of this [GraphQL union][1] in GraphQL schema. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub name: String, + + /// Rust type that this [GraphQL union][1] is represented with. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub ty: syn::Type, + + /// Generics of the Rust type that this [GraphQL union][1] is implemented for. + pub generics: syn::Generics, + + /// Indicator whether code should be generated for a trait object, rather than for a regular + /// Rust type. + pub is_trait_object: bool, + + /// Description of this [GraphQL union][1] to put into GraphQL schema. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub description: Option, + + /// Rust type of `juniper::Context` to generate `juniper::GraphQLType` implementation with + /// for this [GraphQL union][1]. + /// + /// If [`None`] then generated code will use unit type `()` as `juniper::Context`. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub context: Option, + + /// Rust type of `juniper::ScalarValue` to generate `juniper::GraphQLType` implementation with + /// for this [GraphQL union][1]. + /// + /// If [`None`] then generated code will be generic over any `juniper::ScalarValue` type, which, + /// in turn, requires all [union][1] variants to be generic over any `juniper::ScalarValue` type + /// too. That's why this type should be specified only if one of the variants implements + /// `juniper::GraphQLType` in a non-generic way over `juniper::ScalarValue` type. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub scalar: Option, + + /// Variants definitions of this [GraphQL union][1]. + /// + /// [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, + + /// [`Mode`] to generate code in for this [GraphQL union][1]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub mode: Mode, +} + +impl ToTokens for UnionDefinition { + fn to_tokens(&self, into: &mut TokenStream) { + let crate_path = self.mode.crate_path(); + + let name = &self.name; + let ty = &self.ty; + + let context = self + .context + .as_ref() + .map(|ctx| quote! { #ctx }) + .unwrap_or_else(|| quote! { () }); + + let scalar = self + .scalar + .as_ref() + .map(|scl| quote! { #scl }) + .unwrap_or_else(|| quote! { __S }); + let default_scalar = self + .scalar + .as_ref() + .map(|scl| quote! { #scl }) + .unwrap_or_else(|| quote! { #crate_path::DefaultScalarValue }); + + let description = self + .description + .as_ref() + .map(|desc| quote! { .description(#desc) }); + + let var_types: Vec<_> = self.variants.iter().map(|var| &var.ty).collect(); + + let all_variants_unique = if var_types.len() > 1 { + Some(quote! { #crate_path::sa::assert_type_ne_all!(#(#var_types),*); }) + } else { + None + }; + + let match_names = self.variants.iter().map(|var| { + let var_ty = &var.ty; + let var_check = &var.resolver_check; + quote! { + if #var_check { + return <#var_ty as #crate_path::GraphQLType<#scalar>>::name(&()) + .unwrap().to_string(); + } + } + }); + + let match_resolves: Vec<_> = self.variants.iter().map(|var| &var.resolver_code).collect(); + let resolve_into_type = self.variants.iter().zip(match_resolves.iter()).map(|(var, expr)| { + let var_ty = &var.ty; + + let get_name = quote! { (<#var_ty as #crate_path::GraphQLType<#scalar>>::name(&())) }; + quote! { + if type_name == #get_name.unwrap() { + return #crate_path::IntoResolvable::into( + { #expr }, + executor.context() + ) + .and_then(|res| match res { + Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(&(), &r), + None => Ok(#crate_path::Value::null()), + }); + } + } + }); + let resolve_into_type_async = + self.variants + .iter() + .zip(match_resolves.iter()) + .map(|(var, expr)| { + let var_ty = &var.ty; + + let get_name = quote! { + (<#var_ty as #crate_path::GraphQLType<#scalar>>::name(&())) + }; + quote! { + if type_name == #get_name.unwrap() { + let res = #crate_path::IntoResolvable::into( + { #expr }, + executor.context() + ); + return #crate_path::futures::future::FutureExt::boxed(async move { + match res? { + Some((ctx, r)) => { + let subexec = executor.replaced_context(ctx); + subexec.resolve_with_ctx_async(&(), &r).await + }, + None => Ok(#crate_path::Value::null()), + } + }); + } + } + }); + + let (_, ty_generics, _) = self.generics.split_for_impl(); + + let mut base_generics = self.generics.clone(); + if self.is_trait_object { + base_generics.params.push(parse_quote! { '__obj }); + } + let (impl_generics, _, _) = base_generics.split_for_impl(); + + let mut ext_generics = base_generics.clone(); + if self.scalar.is_none() { + ext_generics.params.push(parse_quote! { #scalar }); + ext_generics + .where_clause + .get_or_insert_with(|| parse_quote! { where }) + .predicates + .push(parse_quote! { #scalar: #crate_path::ScalarValue }); + } + let (ext_impl_generics, _, where_clause) = ext_generics.split_for_impl(); + + let mut where_async = where_clause + .cloned() + .unwrap_or_else(|| parse_quote! { where }); + where_async + .predicates + .push(parse_quote! { Self: Send + Sync }); + if self.scalar.is_none() { + where_async + .predicates + .push(parse_quote! { #scalar: Send + Sync }); + } + + let mut ty_full = quote! { #ty#ty_generics }; + if self.is_trait_object { + ty_full = quote! { dyn #ty_full + '__obj + Send + Sync }; + } + + let type_impl = quote! { + #[automatically_derived] + impl#ext_impl_generics #crate_path::GraphQLType<#scalar> for #ty_full + #where_clause + { + type Context = #context; + type TypeInfo = (); + + fn name(_ : &Self::TypeInfo) -> Option<&str> { + Some(#name) + } + + fn meta<'r>( + info: &Self::TypeInfo, + registry: &mut #crate_path::Registry<'r, #scalar> + ) -> #crate_path::meta::MetaType<'r, #scalar> + where #scalar: 'r, + { + let types = &[ + #( registry.get_type::<&#var_types>(&(())), )* + ]; + registry.build_union_type::<#ty_full>(info, types) + #description + .into_meta() + } + + fn concrete_type_name( + &self, + context: &Self::Context, + _: &Self::TypeInfo, + ) -> String { + #( #match_names )* + panic!( + "GraphQL union {} cannot be resolved into any of its variants in its \ + current state", + #name, + ); + } + + fn resolve_into_type( + &self, + _: &Self::TypeInfo, + type_name: &str, + _: Option<&[#crate_path::Selection<#scalar>]>, + executor: &#crate_path::Executor, + ) -> #crate_path::ExecutionResult<#scalar> { + let context = executor.context(); + #( #resolve_into_type )* + panic!( + "Concrete type {} is not handled by instance resolvers on GraphQL union {}", + type_name, #name, + ); + } + } + }; + + let async_type_impl = quote! { + #[automatically_derived] + impl#ext_impl_generics #crate_path::GraphQLTypeAsync<#scalar> for #ty_full + #where_async + { + fn resolve_into_type_async<'b>( + &'b self, + _: &'b Self::TypeInfo, + type_name: &str, + _: Option<&'b [#crate_path::Selection<'b, #scalar>]>, + executor: &'b #crate_path::Executor<'b, 'b, Self::Context, #scalar> + ) -> #crate_path::BoxFuture<'b, #crate_path::ExecutionResult<#scalar>> { + let context = executor.context(); + #( #resolve_into_type_async )* + panic!( + "Concrete type {} is not handled by instance resolvers on GraphQL union {}", + type_name, #name, + ); + } + } + }; + + let output_type_impl = quote! { + #[automatically_derived] + impl#ext_impl_generics #crate_path::marker::IsOutputType<#scalar> for #ty_full + #where_clause + { + fn mark() { + #( <#var_types as #crate_path::marker::GraphQLObjectType<#scalar>>::mark(); )* + } + } + }; + + let union_impl = quote! { + #[automatically_derived] + impl#impl_generics #crate_path::marker::GraphQLUnion for #ty_full { + fn mark() { + #all_variants_unique + + #( <#var_types as #crate_path::marker::GraphQLObjectType< + #default_scalar, + >>::mark(); )* + } + } + }; + + into.append_all(&[union_impl, output_type_impl, type_impl, async_type_impl]); + } +} + +/// Emerges [`UnionMeta::external_resolvers`] into the given [GraphQL union][1] `variants`. +/// +/// If duplication happens, then resolving code is overwritten with the one from +/// `external_resolvers`. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +fn emerge_union_variants_from_meta( + variants: &mut Vec, + external_resolvers: UnionMetaResolvers, + mode: Mode, +) { + if external_resolvers.is_empty() { + return; + } + + let crate_path = mode.crate_path(); + + for (ty, rslvr) in external_resolvers { + let span = rslvr.span_joined(); + + let resolver_fn = rslvr.into_inner(); + let resolver_code = parse_quote! { + #resolver_fn(self, #crate_path::FromContext::from(context)) + }; + // Doing this may be quite an expensive, because resolving may contain some heavy + // computation, so we're preforming it twice. Unfortunately, we have no other options here, + // until the `juniper::GraphQLType` itself will allow to do it in some cleverer way. + let resolver_check = parse_quote! { + ({ #resolver_code } as ::std::option::Option<&#ty>).is_some() + }; + + if let Some(var) = variants.iter_mut().find(|v| v.ty == ty) { + var.resolver_code = resolver_code; + var.resolver_check = resolver_check; + var.span = span; + } else { + variants.push(UnionVariantDefinition { + ty, + resolver_code, + resolver_check, + enum_path: None, + context_ty: None, + span, + }) + } + } +} + +/// Checks whether all [GraphQL union][1] `variants` represent a different Rust type. +/// +/// # Notice +/// +/// This is not an optimal implementation, as it's possible to bypass this check by using a full +/// qualified path instead (`crate::Test` vs `Test`). Since this requirement is mandatory, the +/// static assertion [`assert_type_ne_all!`][2] is used to enforce this requirement in the generated +/// code. However, due to the bad error message this implementation should stay and provide +/// guidance. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +/// [2]: https://docs.rs/static_assertions/latest/static_assertions/macro.assert_type_ne_all.html +fn all_variants_different(variants: &Vec) -> bool { + let mut types: Vec<_> = variants.iter().map(|var| &var.ty).collect(); + types.dedup(); + types.len() == variants.len() +} diff --git a/juniper_codegen/src/impl_object.rs b/juniper_codegen/src/impl_object.rs index 4cee482f..36779267 100644 --- a/juniper_codegen/src/impl_object.rs +++ b/juniper_codegen/src/impl_object.rs @@ -65,7 +65,7 @@ fn create( let _type = match method.sig.output { syn::ReturnType::Type(_, ref t) => *t.clone(), syn::ReturnType::Default => { - error.custom(method.sig.span(), "return value required"); + error.emit_custom(method.sig.span(), "return value required"); return None; } }; @@ -228,6 +228,7 @@ fn create( include_type_generics: false, generic_scalar: false, no_async: _impl.attrs.no_async.is_some(), + mode: is_internal.into(), }; Ok(definition) diff --git a/juniper_codegen/src/impl_scalar.rs b/juniper_codegen/src/impl_scalar.rs index 37a97658..fc449aa8 100644 --- a/juniper_codegen/src/impl_scalar.rs +++ b/juniper_codegen/src/impl_scalar.rs @@ -264,7 +264,7 @@ pub fn build_scalar( executor: &'a #crate_name::Executor, ) -> #crate_name::BoxFuture<'a, #crate_name::ExecutionResult<#async_generic_type>> { use #crate_name::GraphQLType; - use futures::future; + use #crate_name::futures::future; let v = self.resolve(info, selection_set, executor); Box::pin(future::ready(v)) } diff --git a/juniper_codegen/src/impl_union.rs b/juniper_codegen/src/impl_union.rs deleted file mode 100644 index fa9aea4b..00000000 --- a/juniper_codegen/src/impl_union.rs +++ /dev/null @@ -1,223 +0,0 @@ -use crate::{ - result::GraphQLScope, - util::{self, span_container::SpanContainer}, -}; -use proc_macro2::TokenStream; -use quote::quote; -use syn::{ext::IdentExt, spanned::Spanned}; - -struct ResolverVariant { - pub ty: syn::Type, - pub resolver: syn::Expr, -} - -struct ResolveBody { - pub variants: Vec, -} - -impl syn::parse::Parse for ResolveBody { - fn parse(input: syn::parse::ParseStream) -> Result { - input.parse::()?; - input.parse::()?; - - let match_body; - syn::braced!( match_body in input ); - - let mut variants = Vec::new(); - while !match_body.is_empty() { - let ty = match_body.parse::()?; - match_body.parse::()?; - let resolver = match_body.parse::()?; - - variants.push(ResolverVariant { ty, resolver }); - - // Optinal trailing comma. - match_body.parse::().ok(); - } - - if !input.is_empty() { - return Err(input.error("unexpected input")); - } - - Ok(Self { variants }) - } -} - -pub fn impl_union( - is_internal: bool, - attrs: TokenStream, - body: TokenStream, - error: GraphQLScope, -) -> syn::Result { - let body_span = body.span(); - let _impl = util::parse_impl::ImplBlock::parse(attrs, body)?; - - // FIXME: what is the purpose of this construct? - // Validate trait target name, if present. - if let Some((name, path)) = &_impl.target_trait { - if !(name == "GraphQLUnion" || name == "juniper.GraphQLUnion") { - return Err(error.custom_error( - path.span(), - "Invalid impl target trait: expected 'GraphQLUnion'", - )); - } - } - - let type_ident = &_impl.type_ident; - let name = _impl - .attrs - .name - .clone() - .map(SpanContainer::into_inner) - .unwrap_or_else(|| type_ident.unraw().to_string()); - let crate_name = util::juniper_path(is_internal); - - let scalar = _impl - .attrs - .scalar - .as_ref() - .map(|s| quote!( #s )) - .unwrap_or_else(|| { - quote! { #crate_name::DefaultScalarValue } - }); - - let method = _impl - .methods - .iter() - .find(|&m| _impl.parse_resolve_method(&m).is_ok()); - - let method = match method { - Some(method) => method, - None => { - return Err(error.custom_error( - body_span, - "expected exactly one method with signature: fn resolve(&self) { ... }", - )) - } - }; - - let resolve_args = _impl.parse_resolve_method(method)?; - - let stmts = &method.block.stmts; - let body_raw = quote!( #( #stmts )* ); - let body = syn::parse::(body_raw.into())?; - - if body.variants.is_empty() { - error.not_empty(method.span()) - } - - proc_macro_error::abort_if_dirty(); - - let meta_types = body.variants.iter().map(|var| { - let var_ty = &var.ty; - - quote! { - registry.get_type::<&#var_ty>(&(())), - } - }); - - let concrete_type_resolver = body.variants.iter().map(|var| { - let var_ty = &var.ty; - let resolve = &var.resolver; - - quote! { - if ({#resolve} as std::option::Option<&#var_ty>).is_some() { - return <#var_ty as #crate_name::GraphQLType<#scalar>>::name(&()).unwrap().to_string(); - } - } - }); - - let resolve_into_type = body.variants.iter().map(|var| { - let var_ty = &var.ty; - let resolve = &var.resolver; - - quote! { - if type_name == (<#var_ty as #crate_name::GraphQLType<#scalar>>::name(&())).unwrap() { - return executor.resolve(&(), &{ #resolve }); - } - } - }); - - let generics = _impl.generics; - let (impl_generics, _, where_clause) = generics.split_for_impl(); - - let description = match _impl.description.as_ref() { - Some(value) => quote!( .description( #value ) ), - None => quote!(), - }; - let context = _impl - .attrs - .context - .map(|c| quote! { #c }) - .unwrap_or_else(|| quote! { () }); - - let ty = _impl.target_type; - - let object_marks = body.variants.iter().map(|field| { - let _ty = &field.ty; - quote!( - <#_ty as #crate_name::marker::GraphQLObjectType<#scalar>>::mark(); - ) - }); - - let output = quote! { - impl #impl_generics #crate_name::marker::IsOutputType<#scalar> for #ty #where_clause { - fn mark() { - #( #object_marks )* - } - } - - impl #impl_generics #crate_name::GraphQLType<#scalar> for #ty #where_clause - { - type Context = #context; - type TypeInfo = (); - - fn name(_ : &Self::TypeInfo) -> Option<&str> { - Some(#name) - } - - fn meta<'r>( - info: &Self::TypeInfo, - registry: &mut #crate_name::Registry<'r, #scalar> - ) -> #crate_name::meta::MetaType<'r, #scalar> - where - #scalar: 'r, - { - let types = &[ - #( #meta_types )* - ]; - registry.build_union_type::<#ty>( - info, types - ) - #description - .into_meta() - } - - #[allow(unused_variables)] - fn concrete_type_name(&self, context: &Self::Context, _info: &Self::TypeInfo) -> String { - #( #concrete_type_resolver )* - - panic!("Concrete type not handled by instance resolvers on {}", #name); - } - - fn resolve_into_type( - &self, - _info: &Self::TypeInfo, - type_name: &str, - _: Option<&[#crate_name::Selection<#scalar>]>, - executor: &#crate_name::Executor, - ) -> #crate_name::ExecutionResult<#scalar> { - let context = &executor.context(); - #( #resolve_args )* - - #( #resolve_into_type )* - - panic!("Concrete type not handled by instance resolvers on {}", #name); - } - } - - - }; - - Ok(output.into()) -} diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 55ffc108..9e657891 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -16,15 +16,17 @@ mod derive_enum; mod derive_input_object; mod derive_object; mod derive_scalar_value; -mod derive_union; mod impl_object; mod impl_scalar; -mod impl_union; + +mod graphql_union; use proc_macro::TokenStream; -use proc_macro_error::proc_macro_error; +use proc_macro_error::{proc_macro_error, ResultExt as _}; use result::GraphQLScope; +use self::util::Mode; + #[proc_macro_error] #[proc_macro_derive(GraphQLEnum, attributes(graphql))] pub fn derive_enum(input: TokenStream) -> TokenStream { @@ -93,16 +95,6 @@ pub fn derive_object_internal(input: TokenStream) -> TokenStream { } } -#[proc_macro_error] -#[proc_macro_derive(GraphQLUnion, attributes(graphql))] -pub fn derive_union(input: TokenStream) -> TokenStream { - let ast = syn::parse::(input).unwrap(); - let gen = derive_union::build_derive_union(ast, false, GraphQLScope::DeriveUnion); - match gen { - Ok(gen) => gen.into(), - Err(err) => proc_macro_error::abort!(err), - } -} /// This custom derive macro implements the #[derive(GraphQLScalarValue)] /// derive. /// @@ -554,27 +546,616 @@ pub fn graphql_subscription_internal(args: TokenStream, input: TokenStream) -> T )) } +/// `#[derive(GraphQLUnion)]` macro for deriving a [GraphQL union][1] implementation for enums and +/// structs. +/// +/// The `#[graphql]` helper attribute is used for configuring the derived implementation. Specifying +/// multiple `#[graphql]` attributes on the same definition is totally okay. They all will be +/// treated as a single attribute. +/// +/// ``` +/// use derive_more::From; +/// use juniper::{GraphQLObject, GraphQLUnion}; +/// +/// #[derive(GraphQLObject)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// struct Droid { +/// id: String, +/// primary_function: String, +/// } +/// +/// #[derive(From, GraphQLUnion)] +/// enum CharacterEnum { +/// Human(Human), +/// Droid(Droid), +/// } +/// ``` +/// +/// # Custom name and description +/// +/// The name of [GraphQL union][1] may be overriden with a `name` attribute's argument. By default, +/// a type name is used. +/// +/// The description of [GraphQL union][1] may be specified either with a `description`/`desc` +/// attribute's argument, or with a regular Rust doc comment. +/// +/// ``` +/// # use juniper::{GraphQLObject, GraphQLUnion}; +/// # +/// # #[derive(GraphQLObject)] +/// # struct Human { +/// # id: String, +/// # home_planet: String, +/// # } +/// # +/// # #[derive(GraphQLObject)] +/// # struct Droid { +/// # id: String, +/// # primary_function: String, +/// # } +/// # +/// #[derive(GraphQLUnion)] +/// #[graphql(name = "Character", desc = "Possible episode characters.")] +/// enum Chrctr { +/// Human(Human), +/// Droid(Droid), +/// } +/// +/// // NOTICE: Rust docs are used as GraphQL description. +/// /// Possible episode characters. +/// #[derive(GraphQLUnion)] +/// enum CharacterWithDocs { +/// Human(Human), +/// Droid(Droid), +/// } +/// +/// // NOTICE: `description` argument takes precedence over Rust docs. +/// /// Not a GraphQL description anymore. +/// #[derive(GraphQLUnion)] +/// #[graphql(description = "Possible episode characters.")] +/// enum CharacterWithDescription { +/// Human(Human), +/// Droid(Droid), +/// } +/// ``` +/// +/// # Custom context +/// +/// By default, the generated implementation uses [unit type `()`][4] as context. To use a custom +/// context type for [GraphQL union][1] variants types or external resolver functions, specify it +/// with `context`/`Context` attribute's argument. +/// +/// ``` +/// # use juniper::{GraphQLObject, GraphQLUnion}; +/// # +/// #[derive(GraphQLObject)] +/// #[graphql(Context = CustomContext)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// #[graphql(Context = CustomContext)] +/// struct Droid { +/// id: String, +/// primary_function: String, +/// } +/// +/// pub struct CustomContext; +/// impl juniper::Context for CustomContext {} +/// +/// #[derive(GraphQLUnion)] +/// #[graphql(Context = CustomContext)] +/// enum Character { +/// Human(Human), +/// Droid(Droid), +/// } +/// ``` +/// +/// # Custom `ScalarValue` +/// +/// By default, this macro generates code, which is generic over a `ScalarValue` type. +/// This may introduce a problem when at least one of [GraphQL union][1] variants is restricted to a +/// concrete `ScalarValue` type in its implementation. To resolve such problem, a concrete +/// `ScalarValue` type should be specified with a `scalar`/`Scalar`/`ScalarValue` attribute's +/// argument. +/// +/// ``` +/// # use juniper::{DefaultScalarValue, GraphQLObject, GraphQLUnion}; +/// # +/// #[derive(GraphQLObject)] +/// #[graphql(Scalar = DefaultScalarValue)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// struct Droid { +/// id: String, +/// primary_function: String, +/// } +/// +/// // NOTICE: Removing `Scalar` argument will fail compilation. +/// #[derive(GraphQLUnion)] +/// #[graphql(Scalar = DefaultScalarValue)] +/// enum Character { +/// Human(Human), +/// Droid(Droid), +/// } +/// ``` +/// +/// # Ignoring enum variants +/// +/// To omit exposing an enum variant in the GraphQL schema, use an `ignore`/`skip` attribute's +/// argument directly on that variant. +/// +/// > __WARNING__: +/// > It's the _library user's responsibility_ to ensure that ignored enum variant is _never_ +/// > returned from resolvers, otherwise resolving the GraphQL query will __panic at runtime__. +/// +/// ``` +/// # use std::marker::PhantomData; +/// use derive_more::From; +/// use juniper::{GraphQLObject, GraphQLUnion}; +/// +/// #[derive(GraphQLObject)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// struct Droid { +/// id: String, +/// primary_function: String, +/// } +/// +/// #[derive(From, GraphQLUnion)] +/// enum Character { +/// Human(Human), +/// Droid(Droid), +/// #[from(ignore)] +/// #[graphql(ignore)] // or `#[graphql(skip)]`, your choice +/// _State(PhantomData), +/// } +/// ``` +/// +/// # External resolver functions +/// +/// To use a custom logic for resolving a [GraphQL union][1] variant, an external resolver function +/// may be specified with: +/// - either a `with` attribute's argument on an enum variant; +/// - or an `on` attribute's argument on an enum/struct itself. +/// +/// ``` +/// # use juniper::{GraphQLObject, GraphQLUnion}; +/// # +/// #[derive(GraphQLObject)] +/// #[graphql(Context = CustomContext)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// #[graphql(Context = CustomContext)] +/// struct Droid { +/// id: String, +/// primary_function: String, +/// } +/// +/// pub struct CustomContext { +/// droid: Droid, +/// } +/// impl juniper::Context for CustomContext {} +/// +/// #[derive(GraphQLUnion)] +/// #[graphql(Context = CustomContext)] +/// enum Character { +/// Human(Human), +/// #[graphql(with = Character::droid_from_context)] +/// Droid(Droid), +/// } +/// +/// impl Character { +/// // NOTICE: The function signature must contain `&self` and `&Context`, +/// // and return `Option<&VariantType>`. +/// fn droid_from_context<'c>(&self, ctx: &'c CustomContext) -> Option<&'c Droid> { +/// Some(&ctx.droid) +/// } +/// } +/// +/// #[derive(GraphQLUnion)] +/// #[graphql(Context = CustomContext)] +/// #[graphql(on Droid = CharacterWithoutDroid::droid_from_context)] +/// enum CharacterWithoutDroid { +/// Human(Human), +/// #[graphql(ignore)] +/// Droid, +/// } +/// +/// impl CharacterWithoutDroid { +/// fn droid_from_context<'c>(&self, ctx: &'c CustomContext) -> Option<&'c Droid> { +/// if let Self::Droid = self { +/// Some(&ctx.droid) +/// } else { +/// None +/// } +/// } +/// } +/// ``` +/// +/// # Deriving structs +/// +/// Specifying external resolver functions is mandatory for using a struct as a [GraphQL union][1], +/// because this is the only way to declare [GraphQL union][1] variants in this case. +/// +/// ``` +/// # use std::collections::HashMap; +/// # use juniper::{GraphQLObject, GraphQLUnion}; +/// # +/// #[derive(GraphQLObject)] +/// #[graphql(Context = Database)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// #[graphql(Context = Database)] +/// struct Droid { +/// id: String, +/// primary_function: String, +/// } +/// +/// struct Database { +/// humans: HashMap, +/// droids: HashMap, +/// } +/// impl juniper::Context for Database {} +/// +/// #[derive(GraphQLUnion)] +/// #[graphql( +/// Context = Database, +/// on Human = Character::get_human, +/// on Droid = Character::get_droid, +/// )] +/// struct Character { +/// id: String, +/// } +/// +/// impl Character { +/// fn get_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human>{ +/// ctx.humans.get(&self.id) +/// } +/// +/// fn get_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid>{ +/// ctx.droids.get(&self.id) +/// } +/// } +/// ``` +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +/// [4]: https://doc.rust-lang.org/stable/std/primitive.unit.html +#[proc_macro_error] +#[proc_macro_derive(GraphQLUnion, attributes(graphql))] +pub fn derive_union(input: TokenStream) -> TokenStream { + self::graphql_union::derive::expand(input.into(), Mode::Public) + .unwrap_or_abort() + .into() +} + +#[proc_macro_error] +#[proc_macro_derive(GraphQLUnionInternal, attributes(graphql))] +#[doc(hidden)] +pub fn derive_union_internal(input: TokenStream) -> TokenStream { + self::graphql_union::derive::expand(input.into(), Mode::Internal) + .unwrap_or_abort() + .into() +} + +/// `#[graphql_union]` macro for deriving a [GraphQL union][1] implementation for traits. +/// +/// Specifying multiple `#[graphql_union]` attributes on the same definition is totally okay. They +/// all will be treated as a single attribute. +/// +/// A __trait has to be [object safe][2]__, because schema resolvers will need to return a +/// [trait object][3] to specify a [GraphQL union][1] behind it. The [trait object][3] has to be +/// [`Send`] and [`Sync`]. +/// +/// ``` +/// use juniper::{graphql_union, GraphQLObject}; +/// +/// #[derive(GraphQLObject)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// struct Droid { +/// id: String, +/// primary_function: String, +/// } +/// +/// #[graphql_union] +/// trait Character { +/// // NOTICE: The method signature must contain `&self` and return `Option<&VariantType>`. +/// fn as_human(&self) -> Option<&Human> { None } +/// fn as_droid(&self) -> Option<&Droid> { None } +/// } +/// +/// impl Character for Human { +/// fn as_human(&self) -> Option<&Human> { Some(&self) } +/// } +/// +/// impl Character for Droid { +/// fn as_droid(&self) -> Option<&Droid> { Some(&self) } +/// } +/// ``` +/// +/// # Custom name and description +/// +/// The name of [GraphQL union][1] may be overriden with a `name` attribute's argument. By default, +/// a type name is used. +/// +/// The description of [GraphQL union][1] may be specified either with a `description`/`desc` +/// attribute's argument, or with a regular Rust doc comment. +/// +/// ``` +/// # use juniper::{graphql_union, GraphQLObject}; +/// # +/// # #[derive(GraphQLObject)] +/// # struct Human { +/// # id: String, +/// # home_planet: String, +/// # } +/// # +/// # #[derive(GraphQLObject)] +/// # struct Droid { +/// # id: String, +/// # primary_function: String, +/// # } +/// # +/// #[graphql_union(name = "Character", desc = "Possible episode characters.")] +/// trait Chrctr { +/// fn as_human(&self) -> Option<&Human> { None } +/// fn as_droid(&self) -> Option<&Droid> { None } +/// } +/// +/// // NOTICE: Rust docs are used as GraphQL description. +/// /// Possible episode characters. +/// trait CharacterWithDocs { +/// fn as_human(&self) -> Option<&Human> { None } +/// fn as_droid(&self) -> Option<&Droid> { None } +/// } +/// +/// // NOTICE: `description` argument takes precedence over Rust docs. +/// /// Not a GraphQL description anymore. +/// #[graphql_union(description = "Possible episode characters.")] +/// trait CharacterWithDescription { +/// fn as_human(&self) -> Option<&Human> { None } +/// fn as_droid(&self) -> Option<&Droid> { None } +/// } +/// # +/// # impl Chrctr for Human {} +/// # impl Chrctr for Droid {} +/// # impl CharacterWithDocs for Human {} +/// # impl CharacterWithDocs for Droid {} +/// # impl CharacterWithDescription for Human {} +/// # impl CharacterWithDescription for Droid {} +/// ``` +/// +/// # Custom context +/// +/// By default, the generated implementation tries to infer `juniper::Context` type from signatures +/// of trait methods, and uses [unit type `()`][4] if signatures contains no context arguments. +/// +/// If `juniper::Context` type cannot be inferred or is inferred incorrectly, then specify it +/// explicitly with `context`/`Context` attribute's argument. +/// +/// ``` +/// # use std::collections::HashMap; +/// # use juniper::{graphql_union, GraphQLObject}; +/// # +/// #[derive(GraphQLObject)] +/// #[graphql(Context = Database)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// #[graphql(Context = Database)] +/// struct Droid { +/// id: String, +/// primary_function: String, +/// } +/// +/// struct Database { +/// humans: HashMap, +/// droids: HashMap, +/// } +/// impl juniper::Context for Database {} +/// +/// #[graphql_union(Context = Database)] +/// trait Character { +/// fn as_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> { None } +/// fn as_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid> { None } +/// } +/// +/// impl Character for Human { +/// fn as_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> { +/// ctx.humans.get(&self.id) +/// } +/// } +/// +/// impl Character for Droid { +/// fn as_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid> { +/// ctx.droids.get(&self.id) +/// } +/// } +/// ``` +/// +/// # Custom `ScalarValue` +/// +/// By default, `#[graphql_union]` macro generates code, which is generic over a `ScalarValue` type. +/// This may introduce a problem when at least one of [GraphQL union][1] variants is restricted to a +/// concrete `ScalarValue` type in its implementation. To resolve such problem, a concrete +/// `ScalarValue` type should be specified with a `scalar`/`Scalar`/`ScalarValue` attribute's +/// argument. +/// +/// ``` +/// # use juniper::{graphql_union, DefaultScalarValue, GraphQLObject}; +/// # +/// #[derive(GraphQLObject)] +/// #[graphql(Scalar = DefaultScalarValue)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// struct Droid { +/// id: String, +/// primary_function: String, +/// } +/// +/// // NOTICE: Removing `Scalar` argument will fail compilation. +/// #[graphql_union(Scalar = DefaultScalarValue)] +/// trait Character { +/// fn as_human(&self) -> Option<&Human> { None } +/// fn as_droid(&self) -> Option<&Droid> { None } +/// } +/// # +/// # impl Character for Human {} +/// # impl Character for Droid {} +/// ``` +/// +/// # Ignoring trait methods +/// +/// To omit some trait method to be assumed as a [GraphQL union][1] variant and ignore it, use an +/// `ignore`/`skip` attribute's argument directly on that method. +/// +/// ``` +/// # use juniper::{graphql_union, GraphQLObject}; +/// # +/// # #[derive(GraphQLObject)] +/// # struct Human { +/// # id: String, +/// # home_planet: String, +/// # } +/// # +/// # #[derive(GraphQLObject)] +/// # struct Droid { +/// # id: String, +/// # primary_function: String, +/// # } +/// # +/// #[graphql_union] +/// trait Character { +/// fn as_human(&self) -> Option<&Human> { None } +/// fn as_droid(&self) -> Option<&Droid> { None } +/// #[graphql_union(ignore)] // or `#[graphql_union(skip)]`, your choice +/// fn id(&self) -> &str; +/// } +/// # +/// # impl Character for Human { +/// # fn id(&self) -> &str { self.id.as_str() } +/// # } +/// # +/// # impl Character for Droid { +/// # fn id(&self) -> &str { self.id.as_str() } +/// # } +/// ``` +/// +/// # External resolver functions +/// +/// It's not mandatory to use trait methods as [GraphQL union][1] variant resolvers, and instead +/// custom functions may be specified with an `on` attribute's argument. +/// +/// ``` +/// # use std::collections::HashMap; +/// # use juniper::{graphql_union, GraphQLObject}; +/// # +/// #[derive(GraphQLObject)] +/// #[graphql(Context = Database)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// #[graphql(Context = Database)] +/// struct Droid { +/// id: String, +/// primary_function: String, +/// } +/// +/// struct Database { +/// humans: HashMap, +/// droids: HashMap, +/// } +/// impl juniper::Context for Database {} +/// +/// #[graphql_union(Context = Database)] +/// #[graphql_union( +/// on Human = DynCharacter::get_human, +/// on Droid = get_droid, +/// )] +/// trait Character { +/// #[graphql_union(ignore)] +/// fn id(&self) -> &str; +/// } +/// +/// impl Character for Human { +/// fn id(&self) -> &str { self.id.as_str() } +/// } +/// +/// impl Character for Droid { +/// fn id(&self) -> &str { self.id.as_str() } +/// } +/// +/// // NOTICE: The trait object is always `Send` and `Sync`. +/// type DynCharacter<'a> = dyn Character + Send + Sync + 'a; +/// +/// impl<'a> DynCharacter<'a> { +/// fn get_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> { +/// ctx.humans.get(self.id()) +/// } +/// } +/// +/// // NOTICE: Custom resolver function doesn't have to be a method of a type. +/// // It's only a matter of the function signature to match the requirements. +/// fn get_droid<'db>(ch: &DynCharacter<'_>, ctx: &'db Database) -> Option<&'db Droid> { +/// ctx.droids.get(ch.id()) +/// } +/// ``` +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +/// [2]: https://doc.rust-lang.org/stable/reference/items/traits.html#object-safety +/// [3]: https://doc.rust-lang.org/stable/reference/types/trait-object.html +/// [4]: https://doc.rust-lang.org/stable/std/primitive.unit.html #[proc_macro_error] #[proc_macro_attribute] -pub fn graphql_union(attrs: TokenStream, body: TokenStream) -> TokenStream { - let attrs = proc_macro2::TokenStream::from(attrs); - let body = proc_macro2::TokenStream::from(body); - let gen = impl_union::impl_union(false, attrs, body, GraphQLScope::ImplUnion); - match gen { - Ok(gen) => gen.into(), - Err(err) => proc_macro_error::abort!(err), - } +pub fn graphql_union(attr: TokenStream, body: TokenStream) -> TokenStream { + self::graphql_union::attr::expand(attr.into(), body.into(), Mode::Public) + .unwrap_or_abort() + .into() } #[proc_macro_error] #[proc_macro_attribute] #[doc(hidden)] -pub fn graphql_union_internal(attrs: TokenStream, body: TokenStream) -> TokenStream { - let attrs = proc_macro2::TokenStream::from(attrs); - let body = proc_macro2::TokenStream::from(body); - let gen = impl_union::impl_union(true, attrs, body, GraphQLScope::ImplUnion); - match gen { - Ok(gen) => gen.into(), - Err(err) => proc_macro_error::abort!(err), - } +pub fn graphql_union_internal(attr: TokenStream, body: TokenStream) -> TokenStream { + self::graphql_union::attr::expand(attr.into(), body.into(), Mode::Internal) + .unwrap_or_abort() + .into() } diff --git a/juniper_codegen/src/result.rs b/juniper_codegen/src/result.rs index dea2a553..73a49596 100644 --- a/juniper_codegen/src/result.rs +++ b/juniper_codegen/src/result.rs @@ -5,28 +5,29 @@ use proc_macro2::Span; use proc_macro_error::{Diagnostic, Level}; use std::fmt; -pub const GRAPHQL_SPECIFICATION: &'static str = "https://spec.graphql.org/June2018/"; +/// URL of the GraphQL specification (June 2018 Edition). +pub const SPEC_URL: &'static str = "https://spec.graphql.org/June2018/"; #[allow(unused_variables)] pub enum GraphQLScope { + UnionAttr, DeriveObject, DeriveInputObject, - DeriveUnion, + UnionDerive, DeriveEnum, DeriveScalar, - ImplUnion, ImplScalar, ImplObject, } impl GraphQLScope { - pub fn specification_section(&self) -> &str { + pub fn spec_section(&self) -> &str { match self { - GraphQLScope::DeriveObject | GraphQLScope::ImplObject => "#sec-Objects", - GraphQLScope::DeriveInputObject => "#sec-Input-Objects", - GraphQLScope::DeriveUnion | GraphQLScope::ImplUnion => "#sec-Unions", - GraphQLScope::DeriveEnum => "#sec-Enums", - GraphQLScope::DeriveScalar | GraphQLScope::ImplScalar => "#sec-Scalars", + 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", } } } @@ -34,11 +35,11 @@ impl GraphQLScope { impl fmt::Display for GraphQLScope { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let name = match self { - GraphQLScope::DeriveObject | GraphQLScope::ImplObject => "object", - GraphQLScope::DeriveInputObject => "input object", - GraphQLScope::DeriveUnion | GraphQLScope::ImplUnion => "union", - GraphQLScope::DeriveEnum => "enum", - GraphQLScope::DeriveScalar | GraphQLScope::ImplScalar => "scalar", + Self::DeriveObject | Self::ImplObject => "object", + Self::DeriveInputObject => "input object", + Self::UnionAttr | Self::UnionDerive => "union", + Self::DeriveEnum => "enum", + Self::DeriveScalar | Self::ImplScalar => "scalar", }; write!(f, "GraphQL {}", name) @@ -51,20 +52,22 @@ pub enum UnsupportedAttribute { Skip, Interface, Scalar, - Description, Deprecation, Default, } impl GraphQLScope { - fn specification_link(&self) -> String { - format!("{}{}", GRAPHQL_SPECIFICATION, self.specification_section()) + fn spec_link(&self) -> String { + format!("{}{}", SPEC_URL, self.spec_section()) } - pub fn custom>(&self, span: Span, msg: S) { + pub fn custom>(&self, span: Span, msg: S) -> Diagnostic { Diagnostic::spanned(span, Level::Error, format!("{} {}", self, msg.as_ref())) - .note(self.specification_link()) - .emit(); + .note(self.spec_link()) + } + + pub fn emit_custom>(&self, span: Span, msg: S) { + self.custom(span, msg).emit() } pub fn custom_error>(&self, span: Span, msg: S) -> syn::Error { @@ -97,7 +100,7 @@ impl GraphQLScope { Level::Error, format!("{} expects at least one field", self), ) - .note(self.specification_link()) + .note(self.spec_link()) .emit(); } @@ -120,7 +123,7 @@ impl GraphQLScope { ), ) .help(format!("There is at least one other field with the same name `{}`, possibly renamed via the #[graphql] attribute", dup.name)) - .note(self.specification_link()) + .note(self.spec_link()) .emit(); }); }) @@ -130,9 +133,12 @@ impl GraphQLScope { Diagnostic::spanned( field, Level::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.".to_string(), + "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." + .into(), ) - .note(format!("{}#sec-Schema", GRAPHQL_SPECIFICATION)) - .emit(); + .note(format!("{}#sec-Schema", SPEC_URL)) + .emit(); } } diff --git a/juniper_codegen/src/util/mod.rs b/juniper_codegen/src/util/mod.rs index 60ca23de..82351e61 100644 --- a/juniper_codegen/src/util/mod.rs +++ b/juniper_codegen/src/util/mod.rs @@ -1,9 +1,13 @@ #![allow(clippy::single_match)] pub mod duplicate; +pub mod mode; +pub mod option_ext; pub mod parse_impl; pub mod span_container; +use std::ops::Deref as _; + use proc_macro2::{Span, TokenStream}; use proc_macro_error::abort; use quote::quote; @@ -14,10 +18,7 @@ use syn::{ MetaNameValue, NestedMeta, Token, }; -pub fn juniper_path(is_internal: bool) -> syn::Path { - let name = if is_internal { "crate" } else { "juniper" }; - syn::parse_str::(name).unwrap() -} +pub use self::{mode::Mode, option_ext::OptionExt}; /// Returns the name of a type. /// If the type does not end in a simple ident, `None` is returned. @@ -74,6 +75,15 @@ 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, @@ -85,6 +95,16 @@ pub fn find_graphql_attr(attrs: &[Attribute]) -> Option<&Attribute> { .find(|attr| path_eq_single(&attr.path, "graphql")) } +/// Filters given `attrs` to contain attributes only with the given `name`. +pub fn filter_attrs<'a>( + name: &'a str, + attrs: &'a [Attribute], +) -> impl Iterator + 'a { + attrs + .iter() + .filter(move |attr| path_eq_single(&attr.path, name)) +} + pub fn get_deprecated(attrs: &[Attribute]) -> Option> { attrs .iter() @@ -669,6 +689,7 @@ pub struct GraphQLTypeDefiniton { pub generic_scalar: bool, // FIXME: make this redundant. pub no_async: bool, + pub mode: Mode, } impl GraphQLTypeDefiniton { @@ -867,7 +888,7 @@ impl GraphQLTypeDefiniton { Err(e) => Err(e), } }; - use futures::future; + use #juniper_crate_name::futures::future; future::FutureExt::boxed(f) }, ) @@ -884,7 +905,7 @@ impl GraphQLTypeDefiniton { Err(e) => Err(e), } }; - use futures::future; + use #juniper_crate_name::futures::future; future::FutureExt::boxed(f) ) } else { @@ -894,7 +915,7 @@ impl GraphQLTypeDefiniton { Ok(None) => Ok(#juniper_crate_name::Value::null()), Err(e) => Err(e), }; - use futures::future; + use #juniper_crate_name::futures::future; future::FutureExt::boxed(future::ready(v)) ) }; @@ -934,7 +955,7 @@ impl GraphQLTypeDefiniton { ) -> #juniper_crate_name::BoxFuture<'b, #juniper_crate_name::ExecutionResult<#scalar>> where #scalar: Send + Sync, { - use futures::future; + use #juniper_crate_name::futures::future; use #juniper_crate_name::GraphQLType; match field { #( #resolve_matches_async )* @@ -1177,7 +1198,7 @@ impl GraphQLTypeDefiniton { }; quote!( #name => { - futures::FutureExt::boxed(async move { + #juniper_crate_name::futures::FutureExt::boxed(async move { let res #_type = { #code }; let res = #juniper_crate_name::IntoFieldResult::<_, #scalar>::into_result(res)?; let executor= executor.as_owned_executor(); @@ -1267,7 +1288,7 @@ impl GraphQLTypeDefiniton { args: #juniper_crate_name::Arguments<'args, #scalar>, executor: &'ref_e #juniper_crate_name::Executor<'ref_e, 'e, Self::Context, #scalar>, ) -> std::pin::Pin>, #juniper_crate_name::FieldError<#scalar> @@ -1284,7 +1305,7 @@ impl GraphQLTypeDefiniton { 'res: 'f, { use #juniper_crate_name::Value; - use futures::stream::StreamExt as _; + use #juniper_crate_name::futures::stream::StreamExt as _; match field_name { #( #resolve_matches_async )* @@ -1302,245 +1323,6 @@ impl GraphQLTypeDefiniton { ) } - pub fn into_union_tokens(self, juniper_crate_name: &str) -> TokenStream { - let juniper_crate_name = syn::parse_str::(juniper_crate_name).unwrap(); - - let name = &self.name; - let ty = &self._type; - let context = self - .context - .as_ref() - .map(|ctx| quote!( #ctx )) - .unwrap_or_else(|| quote!(())); - - let scalar = self - .scalar - .as_ref() - .map(|s| quote!( #s )) - .unwrap_or_else(|| { - if self.generic_scalar { - // If generic_scalar is true, we always insert a generic scalar. - // See more comments below. - quote!(__S) - } else { - quote!(#juniper_crate_name::DefaultScalarValue) - } - }); - - let description = self - .description - .as_ref() - .map(|description| quote!( .description(#description) )); - - let meta_types = self.fields.iter().map(|field| { - let var_ty = &field._type; - - quote! { - registry.get_type::<&#var_ty>(&(())), - } - }); - - let matcher_variants = self - .fields - .iter() - .map(|field| { - let var_ty = &field._type; - let resolver_code = &field.resolver_code; - - quote!( - #resolver_code(ref x) => <#var_ty as #juniper_crate_name::GraphQLType<#scalar>>::name(&()).unwrap().to_string(), - ) - }); - - let concrete_type_resolver = quote!( - match self { - #( #matcher_variants )* - } - ); - - let matcher_expr: Vec<_> = self - .fields - .iter() - .map(|field| { - let resolver_code = &field.resolver_code; - - quote!( - match self { #resolver_code(ref val) => Some(val), _ => None, } - ) - }) - .collect(); - - let resolve_into_type = self.fields.iter().zip(matcher_expr.iter()).map(|(field, expr)| { - let var_ty = &field._type; - - quote! { - if type_name == (<#var_ty as #juniper_crate_name::GraphQLType<#scalar>>::name(&())).unwrap() { - return #juniper_crate_name::IntoResolvable::into( - { #expr }, - executor.context() - ) - .and_then(|res| { - match res { - Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(&(), &r), - None => Ok(#juniper_crate_name::Value::null()), - } - }); - } - } - }); - - let resolve_into_type_async = self.fields.iter().zip(matcher_expr.iter()).map(|(field, expr)| { - let var_ty = &field._type; - - quote! { - if type_name == (<#var_ty as #juniper_crate_name::GraphQLType<#scalar>>::name(&())).unwrap() { - let inner_res = #juniper_crate_name::IntoResolvable::into( - { #expr }, - executor.context() - ); - - let f = async move { - match inner_res { - Ok(Some((ctx, r))) => { - let subexec = executor.replaced_context(ctx); - subexec.resolve_with_ctx_async(&(), &r).await - }, - Ok(None) => Ok(#juniper_crate_name::Value::null()), - Err(e) => Err(e), - } - }; - use futures::future; - return future::FutureExt::boxed(f); - } - } - }); - - let mut generics = self.generics.clone(); - - if self.scalar.is_none() && self.generic_scalar { - // No custom scalar specified, but always generic specified. - // Therefore we inject the generic scalar. - - generics.params.push(parse_quote!(__S)); - - let where_clause = generics.where_clause.get_or_insert(parse_quote!(where)); - // Insert ScalarValue constraint. - where_clause - .predicates - .push(parse_quote!(__S: #juniper_crate_name::ScalarValue)); - } - - let (impl_generics, _, where_clause) = generics.split_for_impl(); - - let mut where_async = where_clause.cloned().unwrap_or_else(|| parse_quote!(where)); - where_async - .predicates - .push(parse_quote!( #scalar: Send + Sync )); - where_async.predicates.push(parse_quote!(Self: Send + Sync)); - - let async_type_impl = quote!( - impl#impl_generics #juniper_crate_name::GraphQLTypeAsync<#scalar> for #ty - #where_async - { - fn resolve_into_type_async<'b>( - &'b self, - _info: &'b Self::TypeInfo, - type_name: &str, - _: Option<&'b [#juniper_crate_name::Selection<'b, #scalar>]>, - executor: &'b #juniper_crate_name::Executor<'b, 'b, Self::Context, #scalar> - ) -> #juniper_crate_name::BoxFuture<'b, #juniper_crate_name::ExecutionResult<#scalar>> { - let context = &executor.context(); - - #( #resolve_into_type_async )* - - panic!("Concrete type not handled by instance resolvers on {}", #name); - } - } - ); - - let convesion_impls = self.fields.iter().map(|field| { - let variant_ty = &field._type; - let resolver_code = &field.resolver_code; - - quote!( - impl std::convert::From<#variant_ty> for #ty { - fn from(val: #variant_ty) -> Self { - #resolver_code(val) - } - } - ) - }); - - let object_marks = self.fields.iter().map(|field| { - let _ty = &field._type; - quote!( - <#_ty as #juniper_crate_name::marker::GraphQLObjectType<#scalar>>::mark(); - ) - }); - - let mut type_impl = quote! { - #( #convesion_impls )* - - impl #impl_generics #juniper_crate_name::marker::IsOutputType<#scalar> for #ty #where_clause { - fn mark() { - #( #object_marks )* - } - } - - impl #impl_generics #juniper_crate_name::GraphQLType<#scalar> for #ty #where_clause - { - type Context = #context; - type TypeInfo = (); - - fn name(_ : &Self::TypeInfo) -> Option<&str> { - Some(#name) - } - - fn meta<'r>( - info: &Self::TypeInfo, - registry: &mut #juniper_crate_name::Registry<'r, #scalar> - ) -> #juniper_crate_name::meta::MetaType<'r, #scalar> - where - #scalar: 'r, - { - let types = &[ - #( #meta_types )* - ]; - registry.build_union_type::<#ty>( - info, types - ) - #description - .into_meta() - } - - #[allow(unused_variables)] - fn concrete_type_name(&self, context: &Self::Context, _info: &Self::TypeInfo) -> String { - #concrete_type_resolver - } - - fn resolve_into_type( - &self, - _info: &Self::TypeInfo, - type_name: &str, - _: Option<&[#juniper_crate_name::Selection<#scalar>]>, - executor: &#juniper_crate_name::Executor, - ) -> #juniper_crate_name::ExecutionResult<#scalar> { - let context = &executor.context(); - - #( #resolve_into_type )* - - panic!("Concrete type not handled by instance resolvers on {}", #name); - } - } - }; - - if !self.no_async { - type_impl.extend(async_type_impl) - } - - type_impl - } - pub fn into_enum_tokens(self, juniper_crate_name: &str) -> TokenStream { let juniper_crate_name = syn::parse_str::(juniper_crate_name).unwrap(); @@ -1660,7 +1442,7 @@ impl GraphQLTypeDefiniton { executor: &'a #juniper_crate_name::Executor, ) -> #juniper_crate_name::BoxFuture<'a, #juniper_crate_name::ExecutionResult<#scalar>> { use #juniper_crate_name::GraphQLType; - use futures::future; + use #juniper_crate_name::futures::future; let v = self.resolve(info, selection_set, executor); future::FutureExt::boxed(future::ready(v)) } diff --git a/juniper_codegen/src/util/mode.rs b/juniper_codegen/src/util/mode.rs new file mode 100644 index 00000000..c236309b --- /dev/null +++ b/juniper_codegen/src/util/mode.rs @@ -0,0 +1,32 @@ +//! Code generation mode. + +/// Code generation mode for macros. +#[derive(Clone, Copy, Debug)] +pub enum Mode { + /// Generated code is intended to be used by library users. + Public, + + /// Generated code is use only inside the library itself. + Internal, +} + +impl Mode { + pub fn crate_path(&self) -> syn::Path { + syn::parse_str::(match self { + Self::Public => "::juniper", + Self::Internal => "crate", + }) + .unwrap_or_else(|e| proc_macro_error::abort!(e)) + } +} + +// TODO: Remove once all macros are refactored with `Mode`. +impl From for Mode { + fn from(is_internal: bool) -> Self { + if is_internal { + Mode::Internal + } else { + Mode::Public + } + } +} diff --git a/juniper_codegen/src/util/option_ext.rs b/juniper_codegen/src/util/option_ext.rs new file mode 100644 index 00000000..3007abd6 --- /dev/null +++ b/juniper_codegen/src/util/option_ext.rs @@ -0,0 +1,24 @@ +/// 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_codegen/src/util/parse_impl.rs b/juniper_codegen/src/util/parse_impl.rs index 93bc26bd..0aa8392d 100644 --- a/juniper_codegen/src/util/parse_impl.rs +++ b/juniper_codegen/src/util/parse_impl.rs @@ -19,36 +19,6 @@ pub struct ImplBlock { } impl ImplBlock { - /// Parse a `fn resolve()` method declaration found in most - /// generators which rely on `impl` blocks. - pub fn parse_resolve_method( - &self, - method: &syn::ImplItemMethod, - ) -> syn::Result> { - if method.sig.ident != "resolve" { - return Err(syn::Error::new( - method.sig.ident.span(), - "expect the method named `resolve`", - )); - } - - if let syn::ReturnType::Type(_, _) = &method.sig.output { - return Err(syn::Error::new( - method.sig.output.span(), - "method must not have a declared return type", - )); - } - - //NOTICE: `fn resolve()` is a subset of `fn () -> ` - self.parse_method(method, false, |captured, _, _| { - Err(syn::Error::new( - captured.span(), - "only executor or context types are allowed", - )) - }) - .map(|(tokens, _empty)| tokens) - } - /// Parse a `fn () -> ` method declaration found in /// objects. pub fn parse_method< diff --git a/juniper_codegen/src/util/span_container.rs b/juniper_codegen/src/util/span_container.rs index a808bbeb..f335da9f 100644 --- a/juniper_codegen/src/util/span_container.rs +++ b/juniper_codegen/src/util/span_container.rs @@ -1,8 +1,12 @@ +use std::{ + hash::{Hash, Hasher}, + ops, +}; + use proc_macro2::{Span, TokenStream}; use quote::ToTokens; -use std::cmp::{Eq, PartialEq}; -#[derive(Debug, Clone)] +#[derive(Clone, Debug)] pub struct SpanContainer { expr: Option, ident: Span, @@ -24,6 +28,19 @@ impl SpanContainer { self.ident } + pub fn span_joined(&self) -> Span { + if let Some(s) = self.expr { + // TODO: Use `Span::join` once stabilized and available on stable: + // https://github.com/rust-lang/rust/issues/54725 + // self.ident.join(s).unwrap() + + // At the moment, just return the second, more meaningful part. + s + } else { + self.ident + } + } + pub fn into_inner(self) -> T { self.val } @@ -47,7 +64,7 @@ impl AsRef for SpanContainer { } } -impl std::ops::Deref for SpanContainer { +impl ops::Deref for SpanContainer { type Target = T; fn deref(&self) -> &Self::Target { @@ -68,3 +85,12 @@ impl PartialEq for SpanContainer { &self.val == other } } + +impl Hash for SpanContainer { + fn hash(&self, state: &mut H) + where + H: Hasher, + { + self.val.hash(state) + } +}