Make interfaces great again! (#682)

* Bootstrap

* Upd

* Bootstrap macro

* Revert stuff

* Correct PoC to compile

* Bootstrap #[graphql_interface] expansion

* Bootstrap #[graphql_interface] meta parsing

* Bootstrap #[graphql_interface] very basic code generation [skip ci]

* Upd trait code generation and fix keywords usage [skip ci]

* Expand trait impls [skip ci]

* Tune up objects [skip ci]

* Finally! Complies at least... [skip ci]

* Parse meta for fields and its arguments [skip ci]

- also, refactor and bikeshed new macros code

* Impl filling fields meta and bootstrap field resolution [skip ci]

* Poking with fields resolution [skip ci]

* Solve Rust's teen async HRTB problems [skip ci]

* Start parsing trait methods [skip ci]

* Finish parsing fields from trait methods [skip ci]

* Autodetect trait asyncness and allow to specify it [skip ci]

* Allow to autogenerate trait object alias via attribute

* Support generics in trait definition and asyncify them correctly

* Temporary disable explicit async

* Cover arguments and custom names/descriptions in tests

* Re-enable tests with explicit async and fix the codegen to satisfy it

* Check implementers are registered in schema and vice versa

* Check argument camelCases

* Test argument defaults, and allow Into coercions for them

* Re-enable markers

* Re-enable markers and relax Sized requirement on IsInputType/IsOutputType marker traits

* Revert 'juniper_actix' fmt

* Fix missing marks for object

* Fix subscriptions marks

* Deduce result type correctly via traits

* Final fixes

* Fmt

* Restore marks checking

* Support custom ScalarValue

* Cover deprecations with tests

* Impl dowcasting via methods

* Impl dowcasting via external functions

* Support custom context, vol. 1

* Support custom context, vol. 2

* Cover fallible field with test

* Impl explicit generic ScalarValue, vol.1

* Impl explicit generic ScalarValue, vol.2

* Allow passing executor into methods

* Generating enum, vol.1

* Generating enum, vol.2

* Generating enum, vol.3

* Generating enum, vol.3

* Generating enum, vol.4

* Generating enum, vol.5

* Generating enum, vol.6

* Generating enum, vol.7

* Generating enum, vol.8

* Refactor juniper stuff

* Fix juniper tests, vol.1

* Fix juniper tests, vol.2

* Polish 'juniper' crate changes, vol.1

* Polish 'juniper' crate changes, vol.2

* Remove redundant stuf

* Polishing 'juniper_codegen', vol.1

* Polishing 'juniper_codegen', vol.2

* Polishing 'juniper_codegen', vol.3

* Polishing 'juniper_codegen', vol.4

* Polishing 'juniper_codegen', vol.5

* Polishing 'juniper_codegen', vol.6

* Polishing 'juniper_codegen', vol.7

* Polishing 'juniper_codegen', vol.8

* Polishing 'juniper_codegen', vol.9

* Fix other crates tests and make Clippy happier

* Fix examples

* Add codegen failure tests, vol. 1

* Add codegen failure tests, vol. 2

* Add codegen failure tests, vol.3

* Fix codegen failure tests accordingly to latest nightly Rust

* Fix codegen when interface has no implementers

* Fix warnings in book tests

* Describing new interfaces in Book, vol.1

Co-authored-by: Christian Legnitto <LegNeato@users.noreply.github.com>
This commit is contained in:
Kai Ren 2020-10-06 10:21:01 +03:00 committed by GitHub
parent 1e733cc793
commit cbf16c5a33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
108 changed files with 10707 additions and 2550 deletions

View file

@ -30,6 +30,7 @@ result can then be converted to JSON for use with tools and libraries such as
[graphql-client](https://github.com/graphql-rust/graphql-client):
```rust
# #![allow(unused_variables)]
# extern crate juniper;
# extern crate serde_json;
use juniper::{EmptyMutation, EmptySubscription, FieldResult, IntrospectionFormat};

View file

@ -78,6 +78,7 @@ operation returns a [`Future`][Future] with an `Item` value of a `Result<Connect
where [`Connection`][Connection] is a `Stream` of values returned by the operation and [`GraphQLError`][GraphQLError] is the error when the subscription fails.
```rust
# #![allow(dead_code)]
# extern crate futures;
# extern crate juniper;
# extern crate juniper_subscriptions;
@ -88,8 +89,6 @@ where [`Connection`][Connection] is a `Stream` of values returned by the operati
# use juniper_subscriptions::Coordinator;
# use futures::{Stream, StreamExt};
# use std::pin::Pin;
# use tokio::runtime::Runtime;
# use tokio::task;
#
# #[derive(Clone)]
# pub struct Database;

View file

@ -26,6 +26,7 @@ types to a GraphQL schema. The most important one is the
resolvers, which you will use for the `Query` and `Mutation` roots.
```rust
# #![allow(unused_variables)]
# extern crate juniper;
use juniper::{FieldResult, EmptySubscription};

View file

@ -22,6 +22,7 @@ The query root is just a GraphQL object. You define it like any other GraphQL
object in Juniper, most commonly using the `graphql_object` proc macro:
```rust
# #![allow(unused_variables)]
# extern crate juniper;
# use juniper::FieldResult;
# #[derive(juniper::GraphQLObject)] struct User { name: String }
@ -44,6 +45,7 @@ Mutations are _also_ just GraphQL objects. Each mutation is a single field
that performs some mutating side-effect such as updating a database.
```rust
# #![allow(unused_variables)]
# extern crate juniper;
# use juniper::FieldResult;
# #[derive(juniper::GraphQLObject)] struct User { name: String }

View file

@ -5,6 +5,7 @@ GraphQL fields. In Juniper, you can define input objects using a custom derive
attribute, similar to simple objects and enums:
```rust
# #![allow(unused_variables)]
# extern crate juniper;
#[derive(juniper::GraphQLInputObject)]
struct Coordinate {
@ -33,6 +34,7 @@ Just like the [other](objects/defining_objects.md) [derives](enums.md), you can
and add documentation to both the type and the fields:
```rust
# #![allow(unused_variables)]
# extern crate juniper;
#[derive(juniper::GraphQLInputObject)]
#[graphql(name="Coordinate", description="A position on the globe")]

View file

@ -1,16 +1,280 @@
# Interfaces
Interfaces
==========
[GraphQL interfaces][1] map well to interfaces known from common object-oriented languages such as Java or C#, but Rust, unfortunately, has no concept that maps perfectly to them. The nearest analogue of [GraphQL interfaces][1] are Rust traits, and the main difference is that in GraphQL [interface type][1] serves both as an _abstraction_ and a _boxed value (downcastable to concrete implementers)_, while in Rust, a trait is an _abstraction only_ and _to represent such a boxed value a separate type is required_, like enum or trait object, because Rust trait does not represent a type itself, and so can have no values. This difference imposes some unintuitive and non-obvious corner cases when we try to express [GraphQL interfaces][1] in Rust, but on the other hand gives you full control over which type is backing your interface, and how it's resolved.
For implementing [GraphQL interfaces][1] Juniper provides `#[graphql_interface]` macro.
GraphQL interfaces map well to interfaces known from common object-oriented
languages such as Java or C#, but Rust has unfortunately not a concept that maps
perfectly to them. Because of this, defining interfaces in Juniper can require a
little bit of boilerplate code, but on the other hand gives you full control
over which type is backing your interface.
To highlight a couple of different ways you can implement interfaces in Rust,
let's have a look at the same end-result from a few different implementations:
## Traits
Defining a trait is mandatory for defining a [GraphQL interface][1], because this is the _obvious_ way we describe an _abstraction_ in Rust. All [interface][1] fields are defined as computed ones via trait methods.
```rust
# extern crate juniper;
use juniper::graphql_interface;
#[graphql_interface]
trait Character {
fn id(&self) -> &str;
}
#
# fn main() {}
```
However, to return values of such [interface][1], we should provide its implementers and the Rust type representing a _boxed value of this trait_. The last one can be represented in two flavors: enum and [trait object][2].
### Enum values (default)
By default, Juniper generates an enum representing the values of the defined [GraphQL interface][1], and names it straightforwardly, `{Interface}Value`.
```rust
# extern crate juniper;
use juniper::{graphql_interface, GraphQLObject};
#[graphql_interface(for = Human)] // enumerating all implementers is mandatory
trait Character {
fn id(&self) -> &str;
}
#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)] // notice enum name, NOT trait name
struct Human {
id: String,
}
#[graphql_interface] // implementing requires macro attribute too, (°o°)!
impl Character for Human {
fn id(&self) -> &str {
&self.id
}
}
#
# fn main() {
let human = Human { id: "human-32".to_owned() };
// Values type for interface has `From` implementations for all its implementers,
// so we don't need to bother with enum variant names.
let character: CharacterValue = human.into();
assert_eq!(character.id(), "human-32");
# }
```
Also, enum name can be specified explicitly, if desired.
```rust
# extern crate juniper;
use juniper::{graphql_interface, GraphQLObject};
#[graphql_interface(enum = CharaterInterface, for = Human)]
trait Character {
fn id(&self) -> &str;
}
#[derive(GraphQLObject)]
#[graphql(impl = CharaterInterface)]
struct Human {
id: String,
home_planet: String,
}
#[graphql_interface]
impl Character for Human {
fn id(&self) -> &str {
&self.id
}
}
#
# fn main() {}
```
### Trait object values
If, for some reason, we would like to use [trait objects][2] for representing [interface][1] values incorporating dynamic dispatch, that should be specified explicitly in the trait definition.
Downcasting [trait objects][2] in Rust is not that trivial, that's why macro transforms the trait definition slightly, imposing some additional type parameters under-the-hood.
> __NOTICE__:
> A __trait has to be [object safe](https://doc.rust-lang.org/stable/reference/items/traits.html#object-safety)__, because schema resolvers will need to return a [trait object][2] to specify a [GraphQL interface][1] behind it.
```rust
# extern crate juniper;
# extern crate tokio;
use juniper::{graphql_interface, GraphQLObject};
// `dyn` argument accepts the name of type alias for the required trait object,
// and macro generates this alias automatically
#[graphql_interface(dyn = DynCharacter, for = Human)]
trait Character {
async fn id(&self) -> &str; // async fields are supported natively
}
#[derive(GraphQLObject)]
#[graphql(impl = DynCharacter<__S>)] // macro adds `ScalarValue` type parameter to trait,
struct Human { // so it may be specified explicitly when required
id: String,
}
#[graphql_interface(dyn)] // implementing requires to know about dynamic dispatch too
impl Character for Human {
async fn id(&self) -> &str {
&self.id
}
}
#
# #[tokio::main]
# async fn main() {
let human = Human { id: "human-32".to_owned() };
let character: Box<DynCharacter> = Box::new(human);
assert_eq!(character.id().await, "human-32");
# }
```
### Ignoring trait methods
We may want to omit some trait methods to be assumed as [GraphQL interface][1] fields and ignore them.
```rust
# extern crate juniper;
use juniper::{graphql_interface, GraphQLObject};
#[graphql_interface(for = Human)]
trait Character {
fn id(&self) -> &str;
#[graphql_interface(ignore)] // or `#[graphql_interface(skip)]`, your choice
fn ignored(&self) -> u32 { 0 }
}
#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)]
struct Human {
id: String,
}
#[graphql_interface] // implementing requires macro attribute too, (°o°)!
impl Character for Human {
fn id(&self) -> &str {
&self.id
}
}
#
# fn main() {}
```
### Custom context
If a context is required in a trait method to resolve a [GraphQL interface][1] field, specify it as an argument.
```rust
# extern crate juniper;
# use std::collections::HashMap;
use juniper::{graphql_interface, GraphQLObject};
struct Database {
humans: HashMap<String, Human>,
}
impl juniper::Context for Database {}
#[graphql_interface(for = Human)] // look, ma, context type is inferred! (^o^)
trait Character { // while still can be specified via `Context = ...` attribute argument
// If a field argument is named `context` or `ctx`, it's automatically assumed
// as a context argument.
fn id(&self, context: &Database) -> Option<&str>;
// Otherwise, you may mark it explicitly as a context argument.
fn name(&self, #[graphql_interface(context)] db: &Database) -> Option<&str>;
}
#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue, Context = Database)]
struct Human {
id: String,
name: String,
}
#[graphql_interface]
impl Character for Human {
fn id(&self, db: &Database) -> Option<&str> {
if db.humans.contains_key(&self.id) {
Some(&self.id)
} else {
None
}
}
fn name(&self, db: &Database) -> Option<&str> {
if db.humans.contains_key(&self.id) {
Some(&self.name)
} else {
None
}
}
}
#
# fn main() {}
```
### Using executor and explicit generic scalar
If an executor is required in a trait method to resolve a [GraphQL interface][1] field, specify it as an argument.
This requires to explicitly parametrize over [`ScalarValue`][3], as [`Executor`][4] does so.
```rust
# extern crate juniper;
use juniper::{graphql_interface, Executor, GraphQLObject, LookAheadMethods as _, ScalarValue};
#[graphql_interface(for = Human, Scalar = S)] // notice specifying scalar as existing type parameter
trait Character<S: ScalarValue> {
// If a field argument is named `executor`, it's automatically assumed
// as an executor argument.
async fn id<'a>(&self, executor: &'a Executor<'_, '_, (), S>) -> &'a str
where
S: Send + Sync; // required by `#[async_trait]` transformation ¯\_(ツ)_/¯
// Otherwise, you may mark it explicitly as an executor argument.
async fn name<'b>(
&'b self,
#[graphql_interface(executor)] another: &Executor<'_, '_, (), S>,
) -> &'b str
where
S: Send + Sync;
}
#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue<__S>)]
struct Human {
id: String,
name: String,
}
#[graphql_interface(Scalar = S)]
impl<S: ScalarValue> Character<S> for Human {
async fn id<'a>(&self, executor: &'a Executor<'_, '_, (), S>) -> &'a str
where
S: Send + Sync,
{
executor.look_ahead().field_name()
}
async fn name<'b>(&'b self, _: &Executor<'_, '_, (), S>) -> &'b str
where
S: Send + Sync,
{
&self.name
}
}
#
# fn main() {}
```
Traits are maybe the most obvious concept you want to use when building
interfaces. But because GraphQL supports downcasting while Rust doesn't, you'll
have to manually specify how to convert a trait into a concrete type. This can
@ -63,7 +327,7 @@ juniper::graphql_interface!(<'a> &'a dyn Character: () as "Character" where Scal
# fn main() {}
```
The `instance_resolvers` declaration lists all the implementors of the given
The `instance_resolvers` declaration lists all the implementers of the given
interface and how to resolve them.
As you can see, you lose a bit of the point with using traits: you need to list
@ -213,3 +477,12 @@ juniper::graphql_interface!(Character: () where Scalar = <S> |&self| {
# fn main() {}
```
[1]: https://spec.graphql.org/June2018/#sec-Interfaces
[2]: https://doc.rust-lang.org/reference/types/trait-object.html
[3]: https://docs.rs/juniper/latest/juniper/trait.ScalarValue.html
[4]: https://docs.rs/juniper/latest/juniper/struct.Executor.html

View file

@ -10,8 +10,9 @@ example from the last chapter, this is how you would define `Person` using the
macro:
```rust
# #![allow(dead_code)]
# extern crate juniper;
#
struct Person {
name: String,
age: i32,

View file

@ -16,7 +16,7 @@ Most of the time, we just need a trivial and straightforward Rust enum to repres
```rust
# extern crate juniper;
# #[macro_use] extern crate derive_more;
# extern crate derive_more;
use derive_more::From;
use juniper::{GraphQLObject, GraphQLUnion};
@ -53,7 +53,7 @@ As an example, let's consider the situation where we need to bind some type para
```rust
# extern crate juniper;
# #[macro_use] extern crate derive_more;
# extern crate derive_more;
# use std::marker::PhantomData;
use derive_more::From;
use juniper::{GraphQLObject, GraphQLUnion};
@ -88,6 +88,7 @@ enum Character<S> {
If some custom logic is needed to resolve a [GraphQL union][1] variant, you may specify an external function to do so:
```rust
# #![allow(dead_code)]
# extern crate juniper;
use juniper::{GraphQLObject, GraphQLUnion};
@ -132,6 +133,7 @@ impl Character {
With an external resolver function we can even declare a new [GraphQL union][1] variant where the Rust type is absent in the initial enum definition. The attribute syntax `#[graphql(on VariantType = resolver_fn)]` follows the [GraphQL syntax for dispatching union variants](https://spec.graphql.org/June2018/#example-f8163).
```rust
# #![allow(dead_code)]
# extern crate juniper;
use juniper::{GraphQLObject, GraphQLUnion};
@ -289,6 +291,7 @@ impl Character for Droid {
If a context is required in a trait method to resolve a [GraphQL union][1] variant, specify it as an argument.
```rust
# #![allow(unused_variables)]
# extern crate juniper;
# use std::collections::HashMap;
use juniper::{graphql_union, GraphQLObject};
@ -451,6 +454,7 @@ fn get_droid<'db>(ch: &DynCharacter<'_>, ctx: &'db Database) -> Option<&'db Droi
By default, `#[derive(GraphQLUnion)]` and `#[graphql_union]` macros generate code, which is generic over a [`ScalarValue`][2] type. This may introduce a problem when at least one of [GraphQL union][1] variants is restricted to a concrete [`ScalarValue`][2] type in its implementation. To resolve such problem, a concrete [`ScalarValue`][2] type should be specified:
```rust
# #![allow(dead_code)]
# extern crate juniper;
use juniper::{DefaultScalarValue, GraphQLObject, GraphQLUnion};

View file

@ -12,7 +12,7 @@ juniper_subscriptions = { path = "../../../juniper_subscriptions" }
derive_more = "0.99.7"
futures = "0.3"
tokio = { version = "0.2", features = ["rt-core", "blocking", "stream", "rt-util"] }
tokio = { version = "0.2", features = ["blocking", "macros", "rt-core", "rt-util", "stream"] }
iron = "0.5.0"
mount = "0.4.0"

View file

@ -4,7 +4,7 @@ use actix_cors::Cors;
use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer};
use juniper::{
tests::fixtures::starwars::{model::Database, schema::Query},
tests::fixtures::starwars::schema::{Character as _, Database, Query},
DefaultScalarValue, EmptyMutation, FieldError, RootNode,
};
use juniper_actix::{graphql_handler, playground_handler, subscriptions::subscriptions_handler};
@ -76,11 +76,11 @@ impl Subscription {
))
} else {
let random_id = rng.gen_range(1000, 1005).to_string();
let human = context.get_human(&random_id).unwrap();
let human = context.get_human(&random_id).unwrap().clone();
Ok(RandomHuman {
id: human.id().to_owned(),
name: human.name().to_owned(),
name: human.name().unwrap().to_owned(),
})
}
});

View file

@ -0,0 +1,19 @@
use juniper::{graphql_interface, GraphQLObject};
#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)]
pub struct ObjA {
test: String,
}
#[graphql_interface]
impl Character for ObjA {}
#[graphql_interface(for = ObjA)]
trait Character {
fn id(&self, __num: i32) -> &str {
"funA"
}
}
fn main() {}

View file

@ -0,0 +1,19 @@
error: All types and directives defined within a schema must not have a name which begins with `__` (two underscores), as this is used exclusively by GraphQLs introspection system.
--> $DIR/argument_double_underscored.rs:14:18
|
14 | fn id(&self, __num: i32) -> &str {
| ^^^^^
|
= note: https://spec.graphql.org/June2018/#sec-Schema
error[E0412]: cannot find type `CharacterValue` in this scope
--> $DIR/argument_double_underscored.rs:4:18
|
4 | #[graphql(impl = CharacterValue)]
| ^^^^^^^^^^^^^^ not found in this scope
error[E0405]: cannot find trait `Character` in this scope
--> $DIR/argument_double_underscored.rs:10:6
|
10 | impl Character for ObjA {}
| ^^^^^^^^^ not found in this scope

View file

@ -0,0 +1,21 @@
use juniper::{graphql_interface, GraphQLObject};
#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)]
pub struct ObjA {
test: String,
}
#[graphql_interface]
impl Character for ObjA {
fn id(&self, obj: Self) -> &str {
"funA"
}
}
#[graphql_interface(for = ObjA)]
trait Character {
fn id(&self, obj: ObjA) -> &str;
}
fn main() {}

View file

@ -0,0 +1,16 @@
error[E0277]: the trait bound `ObjA: IsInputType<__S>` is not satisfied
--> $DIR/argument_non_input_type.rs:16:1
|
16 | #[graphql_interface(for = ObjA)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IsInputType<__S>` is not implemented for `ObjA`
|
= note: required by `juniper::marker::IsInputType::mark`
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `ObjA: FromInputValue<__S>` is not satisfied
--> $DIR/argument_non_input_type.rs:16:1
|
16 | #[graphql_interface(for = ObjA)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `FromInputValue<__S>` is not implemented for `ObjA`
|
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -0,0 +1,29 @@
use juniper::{graphql_interface, GraphQLObject};
#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)]
pub struct ObjA {
test: String,
}
#[graphql_interface]
impl Character for ObjA {
fn id(&self, _: i32) -> &str {
"funA"
}
fn as_obja(&self) -> Option<&ObjA> {
Some(self)
}
}
#[graphql_interface(for = ObjA)]
#[graphql_interface(on ObjA = downcast_obja)]
trait Character {
fn id(&self, num: i32) -> &str;
#[graphql_interface(downcast)]
fn as_obja(&self) -> Option<&ObjA>;
}
fn main() {}

View file

@ -0,0 +1,20 @@
error: GraphQL interface trait method `as_obja` conflicts with the external downcast function `downcast_obja` declared on the trait to downcast into the implementer type `ObjA`
--> $DIR/downcast_method_conflicts_with_external_downcast_fn.rs:26:5
|
26 | fn as_obja(&self) -> Option<&ObjA>;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: https://spec.graphql.org/June2018/#sec-Interfaces
= note: use `#[graphql_interface(ignore)]` attribute argument to ignore this trait method for interface implementers downcasting
error[E0412]: cannot find type `CharacterValue` in this scope
--> $DIR/downcast_method_conflicts_with_external_downcast_fn.rs:4:18
|
4 | #[graphql(impl = CharacterValue)]
| ^^^^^^^^^^^^^^ not found in this scope
error[E0405]: cannot find trait `Character` in this scope
--> $DIR/downcast_method_conflicts_with_external_downcast_fn.rs:10:6
|
10 | impl Character for ObjA {
| ^^^^^^^^^ not found in this scope

View file

@ -0,0 +1,24 @@
use juniper::{graphql_interface, GraphQLObject};
#[graphql_interface(for = Human)]
trait Character {
fn id(&self) -> i32 {
0
}
#[graphql_interface(downcast)]
fn a(&self, ctx: &(), rand: u8) -> Option<&Human> {
None
}
}
#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)]
pub struct Human {
id: String,
}
#[graphql_interface]
impl Character for Human {}
fn main() {}

View file

@ -0,0 +1,19 @@
error: GraphQL interface expects trait method to accept `&self` only and, optionally, `&Context`
--> $DIR/downcast_method_wrong_input_args.rs:10:10
|
10 | fn a(&self, ctx: &(), rand: u8) -> Option<&Human> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: https://spec.graphql.org/June2018/#sec-Interfaces
error[E0412]: cannot find type `CharacterValue` in this scope
--> $DIR/downcast_method_wrong_input_args.rs:16:18
|
16 | #[graphql(impl = CharacterValue)]
| ^^^^^^^^^^^^^^ not found in this scope
error[E0405]: cannot find trait `Character` in this scope
--> $DIR/downcast_method_wrong_input_args.rs:22:6
|
22 | impl Character for Human {}
| ^^^^^^^^^ not found in this scope

View file

@ -0,0 +1,24 @@
use juniper::{graphql_interface, GraphQLObject};
#[graphql_interface(for = Human)]
trait Character {
fn id(&self) -> i32 {
0
}
#[graphql_interface(downcast)]
fn a(&self, ctx: &(), rand: u8) -> &Human {
unimplemented!()
}
}
#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)]
pub struct Human {
id: String,
}
#[graphql_interface]
impl Character for Human {}
fn main() {}

View file

@ -0,0 +1,19 @@
error: GraphQL interface expects trait method return type to be `Option<&ImplementerType>` only
--> $DIR/downcast_method_wrong_return_type.rs:10:40
|
10 | fn a(&self, ctx: &(), rand: u8) -> &Human {
| ^^^^^^
|
= note: https://spec.graphql.org/June2018/#sec-Interfaces
error[E0412]: cannot find type `CharacterValue` in this scope
--> $DIR/downcast_method_wrong_return_type.rs:16:18
|
16 | #[graphql(impl = CharacterValue)]
| ^^^^^^^^^^^^^^ not found in this scope
error[E0405]: cannot find trait `Character` in this scope
--> $DIR/downcast_method_wrong_return_type.rs:22:6
|
22 | impl Character for Human {}
| ^^^^^^^^^ not found in this scope

View file

@ -0,0 +1,19 @@
use juniper::{graphql_interface, GraphQLObject};
#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)]
pub struct ObjA {
test: String,
}
#[graphql_interface]
impl Character for ObjA {}
#[graphql_interface(for = ObjA)]
trait Character {
fn __id(&self) -> &str {
"funA"
}
}
fn main() {}

View file

@ -0,0 +1,19 @@
error: All types and directives defined within a schema must not have a name which begins with `__` (two underscores), as this is used exclusively by GraphQLs introspection system.
--> $DIR/field_double_underscored.rs:14:8
|
14 | fn __id(&self) -> &str {
| ^^^^
|
= note: https://spec.graphql.org/June2018/#sec-Schema
error[E0412]: cannot find type `CharacterValue` in this scope
--> $DIR/field_double_underscored.rs:4:18
|
4 | #[graphql(impl = CharacterValue)]
| ^^^^^^^^^^^^^^ not found in this scope
error[E0405]: cannot find trait `Character` in this scope
--> $DIR/field_double_underscored.rs:10:6
|
10 | impl Character for ObjA {}
| ^^^^^^^^^ not found in this scope

View file

@ -0,0 +1,24 @@
use juniper::{graphql_interface, GraphQLInputObject, GraphQLObject};
#[derive(GraphQLInputObject)]
pub struct ObjB {
id: i32,
}
#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)]
pub struct ObjA {
test: String,
}
#[graphql_interface]
impl Character for ObjA {}
#[graphql_interface(for = ObjA)]
trait Character {
fn id(&self) -> ObjB {
ObjB { id: 34 }
}
}
fn main() {}

View file

@ -0,0 +1,8 @@
error[E0277]: the trait bound `ObjB: IsOutputType<__S>` is not satisfied
--> $DIR/field_non_output_return_type.rs:17:1
|
17 | #[graphql_interface(for = ObjA)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IsOutputType<__S>` is not implemented for `ObjB`
|
= note: required by `juniper::marker::IsOutputType::mark`
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -0,0 +1,24 @@
use juniper::{graphql_interface, GraphQLObject};
#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)]
pub struct ObjA {
test: String,
}
#[graphql_interface]
impl Character for ObjA {}
#[graphql_interface(for = ObjA)]
trait Character {
fn id(&self) -> &str {
"funA"
}
#[graphql_interface(name = "id")]
fn id2(&self) -> &str {
"funB"
}
}
fn main() {}

View file

@ -0,0 +1,25 @@
error: GraphQL interface must have a different name for each field
--> $DIR/fields_duplicate.rs:13:1
|
13 | / trait Character {
14 | | fn id(&self) -> &str {
15 | | "funA"
16 | | }
... |
21 | | }
22 | | }
| |_^
|
= note: https://spec.graphql.org/June2018/#sec-Interfaces
error[E0412]: cannot find type `CharacterValue` in this scope
--> $DIR/fields_duplicate.rs:4:18
|
4 | #[graphql(impl = CharacterValue)]
| ^^^^^^^^^^^^^^ not found in this scope
error[E0405]: cannot find trait `Character` in this scope
--> $DIR/fields_duplicate.rs:10:6
|
10 | impl Character for ObjA {}
| ^^^^^^^^^ not found in this scope

View file

@ -1,23 +0,0 @@
#[derive(juniper::GraphQLObject)]
#[graphql(scalar = juniper::DefaultScalarValue)]
pub struct ObjA {
test: String,
}
enum Character {
A(ObjA),
}
juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| {
field id(__test: ObjA) -> &str {
match *self {
Character::A(_) => "funA",
}
}
instance_resolvers: |_| {
&ObjA => match *self { Character::A(ref h) => Some(h) },
}
});
fn main() {}

View file

@ -1,27 +0,0 @@
error[E0277]: the trait bound `ObjA: FromInputValue` is not satisfied
--> $DIR/impl_argument_no_object.rs:11:1
|
11 | / juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| {
12 | | field id(__test: ObjA) -> &str {
13 | | match *self {
14 | | Character::A(_) => "funA",
... |
20 | | }
21 | | });
| |___^ the trait `FromInputValue` is not implemented for `ObjA`
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `ObjA: FromInputValue` is not satisfied
--> $DIR/impl_argument_no_object.rs:11:1
|
11 | / juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| {
12 | | field id(__test: ObjA) -> &str {
13 | | match *self {
14 | | Character::A(_) => "funA",
... |
20 | | }
21 | | });
| |___^ the trait `FromInputValue` is not implemented for `ObjA`
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -1,23 +0,0 @@
#[derive(juniper::GraphQLObject)]
#[graphql(scalar = juniper::DefaultScalarValue)]
pub struct ObjA {
test: String,
}
enum Character {
A(ObjA),
}
juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| {
field id(__test: String) -> &str {
match *self {
Character::A(_) => "funA",
}
}
instance_resolvers: |_| {
&ObjA => match *self { Character::A(ref h) => Some(h) },
}
});
fn main() {}

View file

@ -1,12 +0,0 @@
enum Character {}
juniper::graphql_interface!(Character: () where Scalar = <S> |&self| {
field id() -> &str {
match *self {
}
}
instance_resolvers: |_| {}
});
fn main() {}

View file

@ -1,23 +0,0 @@
#[derive(juniper::GraphQLInputObject)]
#[graphql(scalar = juniper::DefaultScalarValue)]
pub struct ObjA {
test: String,
}
enum Character {
A(ObjA),
}
juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| {
field id() -> &str {
match *self {
Character::A(_) => "funA",
}
}
instance_resolvers: |_| {
&ObjA => match *self { Character::A(ref h) => Some(h) },
}
});
fn main() {}

View file

@ -1,23 +0,0 @@
#[derive(juniper::GraphQLObject)]
#[graphql(scalar = juniper::DefaultScalarValue)]
pub struct ObjA {
test: String,
}
enum Character {
A(ObjA),
}
juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| {
field __id() -> &str {
match *self {
Character::A(_) => "funA",
}
}
instance_resolvers: |_| {
&ObjA => match *self { Character::A(ref h) => Some(h) },
}
});
fn main() {}

View file

@ -1,29 +0,0 @@
#[derive(juniper::GraphQLObject)]
#[graphql(scalar = juniper::DefaultScalarValue)]
pub struct ObjA {
test: String,
}
enum Character {
A(ObjA),
}
juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| {
field id() -> &str {
match *self {
Character::A(_) => "funA",
}
}
field id() -> &str {
match *self {
Character::A(_) => "funA",
}
}
instance_resolvers: |_| {
&ObjA => match *self { Character::A(ref h) => Some(h) },
}
});
fn main() {}

View file

@ -0,0 +1,20 @@
use juniper::{graphql_interface, GraphQLInputObject};
#[derive(GraphQLInputObject)]
pub struct ObjA {
test: String,
}
#[graphql_interface]
impl Character for ObjA {
fn id(&self) -> &str {
"funA"
}
}
#[graphql_interface(for = ObjA)]
trait Character {
fn id(&self) -> &str;
}
fn main() {}

View file

@ -0,0 +1,17 @@
error[E0277]: the trait bound `ObjA: GraphQLObjectType<__S>` is not satisfied
--> $DIR/implementer_non_object_type.rs:15:1
|
15 | #[graphql_interface(for = ObjA)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `GraphQLObjectType<__S>` is not implemented for `ObjA`
|
= note: required by `juniper::marker::GraphQLObjectType::mark`
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `ObjA: IsOutputType<__S>` is not satisfied
--> $DIR/implementer_non_object_type.rs:15:1
|
15 | #[graphql_interface(for = ObjA)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IsOutputType<__S>` is not implemented for `ObjA`
|
= note: required by `juniper::marker::IsOutputType::mark`
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -0,0 +1,21 @@
use juniper::{graphql_interface, GraphQLObject};
#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)]
pub struct ObjA {
test: String,
}
#[graphql_interface]
impl Character for ObjA {
fn id(&self) -> &str {
"funA"
}
}
#[graphql_interface(for = [ObjA, ObjA])]
trait Character {
fn id(&self) -> &str;
}
fn main() {}

View file

@ -0,0 +1,17 @@
error: duplicated attribute argument found
--> $DIR/implementers_duplicate_pretty.rs:16:34
|
16 | #[graphql_interface(for = [ObjA, ObjA])]
| ^^^^
error[E0412]: cannot find type `CharacterValue` in this scope
--> $DIR/implementers_duplicate_pretty.rs:4:18
|
4 | #[graphql(impl = CharacterValue)]
| ^^^^^^^^^^^^^^ not found in this scope
error[E0405]: cannot find trait `Character` in this scope
--> $DIR/implementers_duplicate_pretty.rs:10:6
|
10 | impl Character for ObjA {
| ^^^^^^^^^ not found in this scope

View file

@ -0,0 +1,23 @@
use juniper::{graphql_interface, GraphQLObject};
#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)]
pub struct ObjA {
test: String,
}
type ObjAlias = ObjA;
#[graphql_interface]
impl Character for ObjA {
fn id(&self) -> &str {
"funA"
}
}
#[graphql_interface(for = [ObjA, ObjAlias])]
trait Character {
fn id(&self) -> &str;
}
fn main() {}

View file

@ -0,0 +1,21 @@
error[E0119]: conflicting implementations of trait `<CharacterValue as juniper::GraphQLInterface<__S>>::mark::_::{closure#0}::MutuallyExclusive` for type `ObjA`:
--> $DIR/implementers_duplicate_ugly.rs:18:1
|
18 | #[graphql_interface(for = [ObjA, ObjAlias])]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
| first implementation here
| conflicting implementation for `ObjA`
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0119]: conflicting implementations of trait `std::convert::From<ObjA>` for type `CharacterValue`:
--> $DIR/implementers_duplicate_ugly.rs:18:1
|
18 | #[graphql_interface(for = [ObjA, ObjAlias])]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
| first implementation here
| conflicting implementation for `CharacterValue`
|
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -0,0 +1,10 @@
use juniper::graphql_interface;
#[graphql_interface]
trait __Character {
fn id(&self) -> &str {
"funA"
}
}
fn main() {}

View file

@ -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 GraphQLs introspection system.
--> $DIR/name_double_underscored.rs:4:7
|
4 | trait __Character {
| ^^^^^^^^^^^
|
= note: https://spec.graphql.org/June2018/#sec-Schema

View file

@ -0,0 +1,15 @@
use juniper::{graphql_interface, GraphQLObject};
#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)]
pub struct ObjA {
test: String,
}
#[graphql_interface]
impl Character for ObjA {}
#[graphql_interface(for = ObjA)]
trait Character {}
fn main() {}

View file

@ -0,0 +1,19 @@
error: GraphQL interface must have at least one field
--> $DIR/no_fields.rs:13:1
|
13 | trait Character {}
| ^^^^^^^^^^^^^^^^^^
|
= note: https://spec.graphql.org/June2018/#sec-Interfaces
error[E0412]: cannot find type `CharacterValue` in this scope
--> $DIR/no_fields.rs:4:18
|
4 | #[graphql(impl = CharacterValue)]
| ^^^^^^^^^^^^^^ not found in this scope
error[E0405]: cannot find trait `Character` in this scope
--> $DIR/no_fields.rs:10:6
|
10 | impl Character for ObjA {}
| ^^^^^^^^^ not found in this scope

View file

@ -0,0 +1,11 @@
use juniper::{graphql_interface, GraphQLObject};
#[derive(GraphQLObject)]
pub struct ObjA {
test: String,
}
#[graphql_interface(for = ObjA)]
enum Character {}
fn main() {}

View file

@ -0,0 +1,7 @@
error: #[graphql_interface] attribute is applicable to trait definitions and trait implementations only
--> $DIR/wrong_item.rs:8:1
|
8 | #[graphql_interface(for = ObjA)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -1,4 +1,4 @@
error[E0119]: conflicting implementations of trait `<Character as juniper::GraphQLUnion<__S>>::mark::_::{{closure}}#0::MutuallyExclusive` for type `std::string::String`:
error[E0119]: conflicting implementations of trait `<Character as juniper::GraphQLUnion<__S>>::mark::_::{closure#0}::MutuallyExclusive` for type `std::string::String`:
--> $DIR/enum_same_type_ugly.rs:3:10
|
3 | #[derive(GraphQLUnion)]

View file

@ -1,4 +1,4 @@
error: duplicated attribute
error: duplicated attribute argument found
--> $DIR/struct_same_type_pretty.rs:5:14
|
5 | #[graphql(on i32 = Character::b)]

View file

@ -1,4 +1,4 @@
error[E0119]: conflicting implementations of trait `<Character as juniper::GraphQLUnion<__S>>::mark::_::{{closure}}#0::MutuallyExclusive` for type `std::string::String`:
error[E0119]: conflicting implementations of trait `<Character as juniper::GraphQLUnion<__S>>::mark::_::{closure#0}::MutuallyExclusive` for type `std::string::String`:
--> $DIR/struct_same_type_ugly.rs:3:10
|
3 | #[derive(GraphQLUnion)]

View file

@ -1,4 +1,4 @@
error[E0119]: conflicting implementations of trait `<(dyn Character + std::marker::Send + std::marker::Sync + '__obj) as juniper::GraphQLUnion<__S>>::mark::_::{{closure}}#0::MutuallyExclusive` for type `std::string::String`:
error[E0119]: conflicting implementations of trait `<(dyn Character + std::marker::Send + std::marker::Sync + '__obj) as juniper::GraphQLUnion<__S>>::mark::_::{closure#0}::MutuallyExclusive` for type `std::string::String`:
--> $DIR/trait_same_type_ugly.rs:3:1
|
3 | #[graphql_union]

View file

@ -10,6 +10,7 @@ futures = "0.3.1"
juniper = { path = "../../juniper" }
[dev-dependencies]
async-trait = "0.1.39"
serde_json = { version = "1" }
fnv = "1.0.3"
tokio = { version = "0.2", features = ["rt-core", "time", "macros"] }

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,7 @@ mod derive_object;
mod derive_object_with_raw_idents;
mod impl_object;
mod impl_scalar;
mod interface_attr;
mod scalar_value_transparent;
mod union_attr;
mod union_derive;

View file

@ -305,7 +305,7 @@ mod generic {
}
}
mod description_from_doc_comments {
mod description_from_doc_comment {
use super::*;
/// Rust docs.
@ -647,93 +647,6 @@ mod custom_scalar {
}
}
mod inferred_custom_context {
use super::*;
#[graphql_union]
trait Character {
fn as_human(&self, _: &CustomContext) -> Option<&HumanCustomContext> {
None
}
fn as_droid(&self, _: &()) -> Option<&DroidCustomContext> {
None
}
}
impl Character for HumanCustomContext {
fn as_human(&self, _: &CustomContext) -> Option<&HumanCustomContext> {
Some(&self)
}
}
impl Character for DroidCustomContext {
fn as_droid(&self, _: &()) -> Option<&DroidCustomContext> {
Some(&self)
}
}
type DynCharacter<'a> = dyn Character + Send + Sync + 'a;
struct QueryRoot;
#[graphql_object(context = CustomContext)]
impl QueryRoot {
fn character(&self, ctx: &CustomContext) -> Box<DynCharacter<'_>> {
let ch: Box<DynCharacter<'_>> = match ctx {
CustomContext::Human => Box::new(HumanCustomContext {
id: "human-32".to_string(),
home_planet: "earth".to_string(),
}),
CustomContext::Droid => Box::new(DroidCustomContext {
id: "droid-99".to_string(),
primary_function: "run".to_string(),
}),
_ => unimplemented!(),
};
ch
}
}
const DOC: &str = r#"{
character {
... on HumanCustomContext {
humanId: id
homePlanet
}
... on DroidCustomContext {
droidId: id
primaryFunction
}
}
}"#;
#[tokio::test]
async fn resolves_human() {
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &Variables::new(), &CustomContext::Human).await,
Ok((
graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_droid() {
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &Variables::new(), &CustomContext::Droid).await,
Ok((
graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}),
vec![],
)),
);
}
}
mod explicit_custom_context {
use super::*;
@ -821,7 +734,94 @@ mod explicit_custom_context {
}
}
mod ignored_methods {
mod inferred_custom_context {
use super::*;
#[graphql_union]
trait Character {
fn as_human(&self, _: &CustomContext) -> Option<&HumanCustomContext> {
None
}
fn as_droid(&self, _: &()) -> Option<&DroidCustomContext> {
None
}
}
impl Character for HumanCustomContext {
fn as_human(&self, _: &CustomContext) -> Option<&HumanCustomContext> {
Some(&self)
}
}
impl Character for DroidCustomContext {
fn as_droid(&self, _: &()) -> Option<&DroidCustomContext> {
Some(&self)
}
}
type DynCharacter<'a> = dyn Character + Send + Sync + 'a;
struct QueryRoot;
#[graphql_object(context = CustomContext)]
impl QueryRoot {
fn character(&self, ctx: &CustomContext) -> Box<DynCharacter<'_>> {
let ch: Box<DynCharacter<'_>> = match ctx {
CustomContext::Human => Box::new(HumanCustomContext {
id: "human-32".to_string(),
home_planet: "earth".to_string(),
}),
CustomContext::Droid => Box::new(DroidCustomContext {
id: "droid-99".to_string(),
primary_function: "run".to_string(),
}),
_ => unimplemented!(),
};
ch
}
}
const DOC: &str = r#"{
character {
... on HumanCustomContext {
humanId: id
homePlanet
}
... on DroidCustomContext {
droidId: id
primaryFunction
}
}
}"#;
#[tokio::test]
async fn resolves_human() {
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &Variables::new(), &CustomContext::Human).await,
Ok((
graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_droid() {
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &Variables::new(), &CustomContext::Droid).await,
Ok((
graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}),
vec![],
)),
);
}
}
mod ignored_method {
use super::*;
#[graphql_union]

View file

@ -18,43 +18,44 @@ edition = "2018"
[badges]
travis-ci = { repository = "graphql-rust/juniper" }
[[bench]]
name = "bench"
harness = false
path = "benches/bench.rs"
[features]
expose-test-schema = ["anyhow", "serde_json"]
schema-language = ["graphql-parser-integration"]
graphql-parser-integration = ["graphql-parser"]
default = [
"bson",
"chrono",
"schema-language",
"url",
"uuid",
"schema-language",
]
expose-test-schema = ["anyhow", "serde_json"]
graphql-parser-integration = ["graphql-parser"]
scalar-naivetime = []
schema-language = ["graphql-parser-integration"]
[dependencies]
juniper_codegen = { version = "0.14.2", path = "../juniper_codegen" }
anyhow = { default-features = false, version = "1.0.32", optional = true }
async-trait = "0.1.39"
bson = { version = "1.0", optional = true }
chrono = { default-features = false, version = "0.4", optional = true }
fnv = "1.0.3"
futures = { default-features = false, features = ["alloc"], version = "0.3.1" }
futures-enum = "0.1.12"
graphql-parser = { version = "0.3", optional = true }
indexmap = { version = "1.0", features = ["serde-1"] }
serde = { default-features = false, version = "1.0.8", features = ["derive"] }
serde_json = { default-features = false, version = "1.0", optional = true }
serde = { version = "1.0.8", features = ["derive"], default-features = false }
serde_json = { version = "1.0.2", default-features = false, optional = true }
static_assertions = "1.1"
url = { version = "2.0", optional = true }
uuid = { default-features = false, version = "0.8", optional = true }
graphql-parser = { version = "0.3", optional = true }
uuid = { version = "0.8", default-features = false, optional = true }
[dev-dependencies]
bencher = "0.1.2"
pretty_assertions = "0.6.1"
serde_json = { version = "1.0.2" }
tokio = { version = "0.2", features = ["macros", "rt-core", "time"] }
pretty_assertions = "0.6.1"
[[bench]]
name = "bench"
harness = false
path = "benches/bench.rs"

View file

@ -1025,10 +1025,9 @@ where
let mut fragments = vec![];
for def in document.iter() {
match def {
Definition::Fragment(f) => fragments.push(f),
_ => (),
};
if let Definition::Fragment(f) = def {
fragments.push(f)
}
}
let default_variable_values = operation.item.variable_definitions.as_ref().map(|defs| {

View file

@ -7,6 +7,7 @@ use self::input_object::{NamedPublic, NamedPublicWithDescription};
use crate::{
executor::Variables,
graphql_interface, graphql_object,
schema::model::RootNode,
types::scalars::{EmptyMutation, EmptySubscription},
value::{DefaultScalarValue, ParseScalarResult, ParseScalarValue, Value},
@ -22,10 +23,6 @@ enum Sample {
struct Scalar(i32);
struct Interface;
struct Root;
#[crate::graphql_scalar(name = "SampleScalar")]
impl GraphQLScalar for Scalar {
fn resolve(&self) -> Value {
@ -41,23 +38,19 @@ impl GraphQLScalar for Scalar {
}
}
graphql_interface!(Interface: () as "SampleInterface" |&self| {
description: "A sample interface"
field sample_enum() -> Sample as "A sample field in the interface" {
/// A sample interface
#[graphql_interface(name = "SampleInterface", for = Root, scalar = DefaultScalarValue)]
trait Interface {
/// A sample field in the interface
fn sample_enum(&self) -> Sample {
Sample::One
}
}
instance_resolvers: |&_| {
Root => Some(Root),
}
});
struct Root;
/// The root query object in the schema
#[crate::graphql_object(
interfaces = [&Interface]
Scalar = crate::DefaultScalarValue,
)]
#[graphql_object(interfaces = InterfaceValue)]
impl Root {
fn sample_enum() -> Sample {
Sample::One
@ -65,7 +58,7 @@ impl Root {
#[graphql(arguments(
first(description = "The first number",),
second(description = "The second number", default = 123,),
second(description = "The second number", default = 123),
))]
/// A sample scalar field on the object
@ -74,6 +67,9 @@ impl Root {
}
}
#[graphql_interface(scalar = DefaultScalarValue)]
impl Interface for Root {}
#[tokio::test]
async fn test_execution() {
let doc = r#"

View file

@ -119,7 +119,7 @@ extern crate bson;
// These are required by the code generated via the `juniper_codegen` macros.
#[doc(hidden)]
pub use {futures, static_assertions as sa};
pub use {async_trait::async_trait, futures, static_assertions as sa};
#[doc(inline)]
pub use futures::future::{BoxFuture, LocalBoxFuture};
@ -128,8 +128,8 @@ pub use futures::future::{BoxFuture, LocalBoxFuture};
// This allows users to just depend on juniper and get the derive
// functionality automatically.
pub use juniper_codegen::{
graphql_object, graphql_scalar, graphql_subscription, graphql_union, GraphQLEnum,
GraphQLInputObject, GraphQLObject, GraphQLScalarValue, GraphQLUnion,
graphql_interface, graphql_object, graphql_scalar, graphql_subscription, graphql_union,
GraphQLEnum, GraphQLInputObject, GraphQLObject, GraphQLScalarValue, GraphQLUnion,
};
#[macro_use]
@ -175,15 +175,18 @@ pub use crate::{
LookAheadSelection, LookAheadValue, OwnedExecutor, Registry, ValuesStream, Variables,
},
introspection::IntrospectionFormat,
macros::subscription_helpers::{ExtractTypeFromStream, IntoFieldResult},
macros::helper::{
subscription::{ExtractTypeFromStream, IntoFieldResult},
AsDynGraphQLValue,
},
schema::{
meta,
model::{RootNode, SchemaType},
},
types::{
async_await::{GraphQLTypeAsync, GraphQLValueAsync},
base::{Arguments, GraphQLType, GraphQLValue, TypeKind},
marker::{self, GraphQLUnion, IsOutputType},
async_await::{DynGraphQLValueAsync, GraphQLTypeAsync, GraphQLValueAsync},
base::{Arguments, DynGraphQLValue, GraphQLType, GraphQLValue, TypeKind},
marker::{self, GraphQLInterface, GraphQLUnion},
scalars::{EmptyMutation, EmptySubscription, ID},
subscriptions::{
ExecutionOutput, GraphQLSubscriptionType, GraphQLSubscriptionValue,

View file

@ -1,741 +0,0 @@
#[doc(hidden)]
#[macro_export]
macro_rules! __juniper_impl_trait {
(
impl< < DefaultScalarValue > $(, $other: tt)* > $impl_trait:tt for $name:ty {
$($body:tt)+
}
) => {
impl<$($other,)*> $crate::$impl_trait<$crate::DefaultScalarValue> for $name {
$($body)*
}
};
(
impl< < DefaultScalarValue > $(, $other: tt)* > $impl_trait:tt for $name:ty
where ( $($where:tt)* )
{
$($body:tt)+
}
) => {
impl<$($other,)*> $crate::$impl_trait<$crate::DefaultScalarValue> for $name
where $($where)*
{
$($body)*
}
};
(
impl< <$generic:tt $(: $bound: tt)*> $(, $other: tt)* > $impl_trait:tt for $name:ty {
$($body:tt)*
}
) => {
impl<$($other,)* $generic $(: $bound)*> $crate::$impl_trait<$generic> for $name
where
$generic: $crate::ScalarValue,
{
$($body)*
}
};
(
impl< <$generic:tt $(: $bound: tt)*> $(, $other: tt)* > $impl_trait:tt for $name:ty
where ( $($where:tt)* )
{
$($body:tt)*
}
) => {
impl<$($other,)* $generic $(: $bound)*> $crate::$impl_trait<$generic> for $name
where
$($where)*
$generic: $crate::ScalarValue,
{
$($body)*
}
};
(
impl<$scalar:ty $(, $other: tt )*> $impl_trait:tt for $name:ty {
$($body:tt)*
}
) => {
impl<$($other, )*> $crate::$impl_trait<$scalar> for $name {
$($body)*
}
};
(
impl<$scalar:ty $(, $other: tt )*> $impl_trait:tt for $name:ty
where ( $($where:tt)* )
{
$($body:tt)*
}
) => {
impl<$($other, )*> $crate::$impl_trait<$scalar> for $name
where $($where)*
{
$($body)*
}
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __juniper_insert_generic {
(<DefaultScalarValue>) => {
$crate::DefaultScalarValue
};
(
<$generic:tt $(: $bound: tt)*>
) => {
$generic
};
(
$scalar: ty
) => {
$scalar
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __juniper_parse_object_header {
(
callback = $callback:ident,
rest = <$($lifetime:tt),*> $name: ty $(: $ctxt: ty)* as $outname: tt
where Scalar = <$generic:tt $(: $bound:tt)*> $(| &$mainself:ident |)* {
$($items: tt)*
}
) => {
$crate::$callback!(
@parse,
meta = {
lifetimes = [$($lifetime,)*],
name = $name,
$(ctx = $ctxt,)*
$(main_self = $mainself,)*
outname = {$outname},
scalar = {<$generic $(: $bound)*>},
},
rest = $($items)*
);
};
(
callback = $callback:ident,
rest = <$($lifetime:tt),*> $name: ty $(: $ctxt: ty)* as $outname: tt
where Scalar = $scalar: ty $(| &$mainself:ident |)* {
$($items: tt)*
}
) => {
$crate::$callback!(
@parse,
meta = {
lifetimes = [$($lifetime,)*],
name = $name,
$(ctx = $ctxt,)*
$(main_self = $mainself,)*
outname = {$outname},
scalar = {$scalar},
},
rest = $($items)*
);
};
(
callback = $callback: ident,
rest = <$($lifetime:tt),*> $name: ty $(: $ctxt: ty)* as $outname: tt $(| &$mainself:ident |)* {
$($items: tt)*
}
) => {
$crate::$callback!(
@parse,
meta = {
lifetimes = [$($lifetime,)*],
name = $name,
$(ctx = $ctxt,)*
$(main_self = $mainself,)*
outname = {$outname},
scalar = {<DefaultScalarValue>},
},
rest = $($items)*
);
};
(
callback = $callback: ident,
rest = $name: ty $(: $ctxt: ty)* as $outname: tt
where Scalar = <$generic:tt $(: $bound:tt)*> $(| &$mainself:ident |)* {
$($items: tt)*
}
) => {
$crate::$callback!(
@parse,
meta = {
lifetimes = [],
name = $name,
$(ctx = $ctxt,)*
$(main_self = $mainself,)*
outname = {$outname},
scalar = {<$generic $(:$bound)*>},
},
rest = $($items)*
);
};
(
callback = $callback: ident,
rest = $name: ty $(: $ctxt: ty)* as $outname: tt
where Scalar = $scalar: ty $(| &$mainself:ident |)* {
$($items: tt)*
}
) => {
$crate::$callback!(
@parse,
meta = {
lifetimes = [],
name = $name,
$(ctx = $ctxt,)*
$(main_self = $mainself,)*
outname = {$outname},
scalar = {$scalar},
},
rest = $($items)*
);
};
(
callback = $callback: ident,
rest = $name: ty $(: $ctxt: ty)* as $outname: tt $(| &$mainself:ident |)* {
$($items: tt)*
}
) => {
$crate::$callback!(
@parse,
meta = {
lifetimes = [],
name = $name,
$(ctx = $ctxt,)*
$(main_self = $mainself,)*
outname = {$outname},
scalar = {<DefaultScalarValue>},
},
rest = $($items)*
);
};
(
callback = $callback: ident,
rest = <$($lifetime:tt),*> $name: ty $(: $ctxt: ty)*
where Scalar = <$generic:tt $(: $bound:tt)*> $(| &$mainself:ident |)* {
$($items: tt)*
}
) => {
$crate::$callback!(
@parse,
meta = {
lifetimes = [$($lifetime,)*],
name = $name,
$(ctx = $ctxt,)*
$(main_self = $mainself,)*
outname = {stringify!($name)},
scalar = {<$generic $(:$bounds)*>},
},
rest = $($items)*
);
};
(
callback = $callback: ident,
rest = <$($lifetime:tt),*> $name: ty $(: $ctxt: ty)*
where Scalar = $scalar: ty $(| &$mainself:ident |)* {
$($items: tt)*
}
) => {
$crate::$callback!(
@parse,
meta = {
lifetimes = [$($lifetime,)*],
name = $name,
$(ctx = $ctxt,)*
$(main_self = $mainself,)*
outname = {stringify!($name)},
scalar = {$scalar},
},
rest = $($items)*
);
};
(
callback = $callback: ident,
rest = <$($lifetime:tt),*> $name: ty $(: $ctxt: ty)* $(| &$mainself:ident |)* {
$($items: tt)*
}
) => {
$crate::$callback!(
@parse,
meta = {
lifetimes = [$($lifetime,)*],
name = $name,
$(ctx = $ctxt,)*
$(main_self = $mainself,)*
outname = {stringify!($name)},
scalar = {<DefaultScalarValue>},
},
rest = $($items)*
);
};
(
callback = $callback: ident,
rest = $name: ty $(: $ctxt: ty)*
where Scalar = <$generic:tt $(: $bound:tt)*> $(| &$mainself:ident |)*
{
$($items: tt)*
}
) => {
$crate::$callback!(
@parse,
meta = {
lifetimes = [],
name = $name,
$(ctx = $ctxt,)*
$(main_self = $mainself,)*
outname = {stringify!($name)},
scalar = {<$generic $(: $bound)*>},
},
rest = $($items)*
);
};
(
callback = $callback: ident,
rest = $name: ty $(: $ctxt: ty)* where Scalar = $scalar: ty $(| &$mainself:ident |)* {
$($items: tt)*
}
) => {
$crate::$callback!(
@parse,
meta = {
lifetimes = [],
name = $name,
$(ctx = $ctxt,)*
$(main_self = $mainself,)*
outname = {stringify!($name)},
scalar = {$scalar},
},
rest = $($items)*
);
};
(
callback = $callback: ident,
rest = $name: ty $(: $ctxt: ty)* $(| &$mainself:ident |)* {
$($items: tt)*
}
) => {
$crate::$callback!(
@parse,
meta = {
lifetimes = [],
name = $name,
$(ctx = $ctxt,)*
$(main_self = $mainself,)*
outname = {stringify!($name)},
scalar = {<DefaultScalarValue>},
},
rest = $($items)*
);
};
(
callback = $callback: ident,
rest = $($rest:tt)*
) => {
compile_error!("Invalid syntax");
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __juniper_parse_field_list {
(
success_callback = $success_callback: ident,
additional_parser = {$($additional:tt)*},
meta = {$($meta:tt)*},
items = [$({$($items: tt)*},)*],
rest =
) => {
$crate::$success_callback!(
@generate,
meta = {$($meta)*},
items = [$({$($items)*},)*],
);
};
(
success_callback = $success_callback: ident,
additional_parser = {$($additional:tt)*},
meta = {$($meta:tt)*},
items = [$({$($items: tt)*},)*],
rest = , $($rest: tt)*
) => {
$crate::__juniper_parse_field_list!(
success_callback = $success_callback,
additional_parser = {$($additional)*},
meta = {$($meta)*},
items = [$({$($items)*},)*],
rest = $($rest)*
);
};
(
@parse_description,
success_callback = $success_callback: ident,
additional_parser = {$($additional:tt)*},
meta = {
$(lifetimes = [$($lifetime:tt,)*],)*
$(name = $name:ty,)*
$(ctx = $ctxt: ty,)*
$(main_self = $mainself: ident,)*
$(outname = {$($outname:tt)*},)*
$(scalar = {$($scalar:tt)*},)*
$(description = $_desciption: tt,)*
$(additional = {$($other: tt)*},)*
},
items = [$({$($items: tt)*},)*],
rest = $desc: tt $($rest:tt)*
) => {
$crate::__juniper_parse_field_list!(
success_callback = $success_callback,
additional_parser = {$($additional)*},
meta = {
$(lifetimes = [$($lifetime,)*],)*
$(name = $name,)*
$(ctx = $ctxt,)*
$(main_self = $mainself,)*
$(outname = {$($outname)*},)*
$(scalar = {$($scalar)*},)*
description = $desc,
$(additional = {$($other)*},)*
},
items = [$({$($items)*},)*],
rest = $($rest)*
);
};
(
success_callback = $success_callback: ident,
additional_parser = {$($additional:tt)*},
meta = { $($meta:tt)*},
items = [$({$($items: tt)*},)*],
rest = description: $($rest:tt)*
) => {
$crate::__juniper_parse_field_list!(
@parse_description,
success_callback = $success_callback,
additional_parser = {$($additional)*},
meta = {$($meta)*},
items = [$({$($items)*},)*],
rest = $($rest)*
);
};
(
success_callback = $success_callback: ident,
additional_parser = {$($additional:tt)*},
meta = {$($meta:tt)*},
items = [$({$($items: tt)*},)*],
rest = $(#[doc = $desc: tt])*
#[deprecated $(( $(since = $since: tt,)* note = $reason: tt ))* ]
field $name: ident (
$(&$executor: tt)* $(,)*
$($(#[doc = $arg_desc: expr])* $arg_name:ident $(= $arg_default: tt)* : $arg_ty: ty),* $(,)*
) -> $return_ty: ty $body: block
$($rest:tt)*
) => {
$crate::__juniper_parse_field_list!(
success_callback = $success_callback,
additional_parser = {$($additional)*},
meta = {$($meta)*},
items = [$({$($items)*},)* {
name = $name,
body = $body,
return_ty = $return_ty,
args = [
$({
arg_name = $arg_name,
arg_ty = $arg_ty,
$(arg_default = $arg_default,)*
$(arg_docstring = $arg_desc,)*
},)*
],
$(docstring = $desc,)*
deprecated = None$(.unwrap_or_else(|| Some($reason)))*,
$(executor_var = $executor,)*
},],
rest = $($rest)*
);
};
(
success_callback = $success_callback: ident,
additional_parser = {$($additional:tt)*},
meta = {$($meta:tt)*},
items = [$({$($items: tt)*},)*],
rest = $(#[doc = $desc: tt])*
field $name: ident (
$(&$executor: ident)* $(,)*
$($(#[doc = $arg_desc: expr])* $arg_name:ident $(= $arg_default: tt)* : $arg_ty: ty),* $(,)*
) -> $return_ty: ty $body: block
$($rest:tt)*
) => {
$crate::__juniper_parse_field_list!(
success_callback = $success_callback,
additional_parser = {$($additional)*},
meta = {$($meta)*},
items = [$({$($items)*},)* {
name = $name,
body = $body,
return_ty = $return_ty,
args = [
$({
arg_name = $arg_name,
arg_ty = $arg_ty,
$(arg_default = $arg_default,)*
$(arg_docstring = $arg_desc,)*
},)*
],
$(docstring = $desc,)*
$(executor_var = $executor,)*
},],
rest = $($rest)*
);
};
(
success_callback = $success_callback: ident,
additional_parser = {$($additional:tt)*},
meta = {$($meta:tt)*},
items = [$({$($items: tt)*},)*],
rest = field deprecated $reason:tt $name: ident (
$(&$executor: tt)* $(,)*
$($arg_name:ident $(= $arg_default: tt)* : $arg_ty: ty $(as $arg_desc: expr)*),* $(,)*
) -> $return_ty: ty $(as $desc: tt)* $body: block
$($rest:tt)*
) => {
$crate::__juniper_parse_field_list!(
success_callback = $success_callback,
additional_parser = {$($additional)*},
meta = {$($meta)*},
items = [$({$($items)*},)* {
name = $name,
body = $body,
return_ty = $return_ty,
args = [
$({
arg_name = $arg_name,
arg_ty = $arg_ty,
$(arg_default = $arg_default,)*
$(arg_description = $arg_desc,)*
},)*
],
$(decs = $desc,)*
deprecated = Some($reason),
$(executor_var = $executor,)*
},],
rest = $($rest)*
);
};
(
success_callback = $success_callback: ident,
additional_parser = {$($additional:tt)*},
meta = {$($meta:tt)*},
items = [$({$($items: tt)*},)*],
rest = field $name: ident (
$(&$executor: ident)* $(,)*
$($arg_name:ident $(= $arg_default: tt)* : $arg_ty: ty $(as $arg_desc: expr)*),* $(,)*
) -> $return_ty: ty $(as $desc: tt)* $body: block
$($rest:tt)*
) => {
$crate::__juniper_parse_field_list!(
success_callback = $success_callback,
additional_parser = {$($additional)*},
meta = {$($meta)*},
items = [$({$($items)*},)* {
name = $name,
body = $body,
return_ty = $return_ty,
args = [
$({
arg_name = $arg_name,
arg_ty = $arg_ty,
$(arg_default = $arg_default,)*
$(arg_description = $arg_desc,)*
},)*
],
$(decs = $desc,)*
$(executor_var = $executor,)*
},],
rest = $($rest)*
);
};
(
success_callback = $success_callback: ident,
additional_parser = {
callback = $callback: ident,
header = {$($header:tt)*},
},
meta = {$($meta:tt)*},
items = [$({$($items: tt)*},)*],
rest = $($rest:tt)*
) => {
$crate::$callback!(
$($header)*
success_callback = $success_callback,
additional_parser = {
callback = $callback,
header = {$($header)*},
},
meta = {$($meta)*},
items = [$({$($items)*},)*],
rest = $($rest)*
);
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! __juniper_parse_instance_resolver {
(
success_callback = $success_callback: ident,
additional_parser = {$($additional:tt)*},
meta = {
lifetimes = [$($lifetime:tt,)*],
name = $name:ty,
ctx = $ctxt:ty,
main_self = $mainself:ident,
outname = {$($outname:tt)*},
scalar = {$($scalar:tt)*},
$(description = $desciption:tt,)*
$(additional = {
$(resolver = {$($ignored_resolver:tt)*},)*
},)*
},
items = [$({$($items: tt)*},)*],
rest = instance_resolvers: |&$context: ident| {
$( $srctype:ty => $resolver:expr ),* $(,)*
} $($rest:tt)*
) => {
$crate::__juniper_parse_field_list!(
success_callback = $success_callback,
additional_parser = {$($additional)*},
meta = {
lifetimes = [$($lifetime,)*],
name = $name,
ctx = $ctxt,
main_self = $mainself,
outname = {$($outname)*},
scalar = {$($scalar)*},
$(description = $desciption,)*
additional = {
resolver = {
context = $context,
items = [
$({
src = $srctype,
resolver = $resolver,
},)*
],
},
},
},
items = [$({$($items)*},)*],
rest = $($rest)*
);
};
(
success_callback = $success_callback: ident,
additional_parser = {$($additional:tt)*},
meta = {
lifetimes = [$($lifetime:tt,)*],
name = $name:ty,
ctx = $ctxt:ty,
main_self = $mainself:ident,
outname = {$($outname:tt)*},
scalar = {$($scalar:tt)*},
$(description = $desciption:tt,)*
$(additional = {
$(resolver = {$($ignored_resolver:tt)*},)*
},)*
},
items = [$({$($items: tt)*},)*],
rest = instance_resolvers: |$(&)* _| {$( $srctype:ty => $resolver:expr ),* $(,)*} $($rest:tt)*
) => {
$crate::__juniper_parse_field_list!(
success_callback = $success_callback,
additional_parser = {$($additional)*},
meta = {
lifetimes = [$($lifetime,)*],
name = $name,
ctx = $ctxt,
main_self = $mainself,
outname = {$($outname)*},
scalar = {$($scalar)*},
$(description = $desciption,)*
additional = {
resolver = {
items = [
$({
src = $srctype,
resolver = $resolver,
},)*
],
},
},
},
items = [$({$($items)*},)*],
rest = $($rest)*
);
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __juniper_create_arg {
(
registry = $reg: ident,
info = $info: ident,
arg_ty = $arg_ty: ty,
arg_name = $arg_name: ident,
$(description = $arg_description: expr,)*
$(docstring = $arg_docstring: expr,)*
) => {
$reg.arg::<$arg_ty>(
&$crate::to_camel_case(stringify!($arg_name)),
$info,
)
$(.description($arg_description))*
.push_docstring(&[$($arg_docstring,)*])
};
(
registry = $reg: ident,
info = $info: ident,
arg_ty = $arg_ty: ty,
arg_name = $arg_name: ident,
default = $arg_default: expr,
$(description = $arg_description: expr,)*
$(docstring = $arg_docstring: expr,)*
) => {
$reg.arg_with_default::<$arg_ty>(
&$crate::to_camel_case(stringify!($arg_name)),
&($arg_default),
$info,
)
$(.description($arg_description))*
.push_docstring(&[$($arg_docstring,)*])
};
}

View file

@ -0,0 +1,34 @@
//! Helper traits and definitions for macros.
pub mod subscription;
use crate::{DefaultScalarValue, DynGraphQLValue, DynGraphQLValueAsync, ScalarValue};
/// Conversion of a [`GraphQLValue`] to its [trait object][1].
///
/// [`GraphQLValue`]: crate::GraphQLValue
/// [1]: https://doc.rust-lang.org/reference/types/trait-object.html
pub trait AsDynGraphQLValue<S: ScalarValue = DefaultScalarValue> {
/// Context type of this [`GraphQLValue`].
///
/// [`GraphQLValue`]: crate::GraphQLValue
type Context;
/// Schema information type of this [`GraphQLValue`].
///
/// [`GraphQLValue`]: crate::GraphQLValue
type TypeInfo;
/// Converts this value to a [`DynGraphQLValue`] [trait object][1].
///
/// [1]: https://doc.rust-lang.org/reference/types/trait-object.html
fn as_dyn_graphql_value(&self) -> &DynGraphQLValue<S, Self::Context, Self::TypeInfo>;
/// Converts this value to a [`DynGraphQLValueAsync`] [trait object][1].
///
/// [1]: https://doc.rust-lang.org/reference/types/trait-object.html
fn as_dyn_graphql_value_async(&self)
-> &DynGraphQLValueAsync<S, Self::Context, Self::TypeInfo>;
}
crate::sa::assert_obj_safe!(AsDynGraphQLValue<Context = (), TypeInfo = ()>);

View file

@ -1,302 +0,0 @@
/**
Expose GraphQL interfaces
Mapping interfaces to GraphQL can be tricky: there is no direct counterpart to
GraphQL interfaces in Rust, and downcasting is not possible in the general case.
Many other GraphQL implementations in other languages use instance checks and
either dynamic typing or forced downcasts to support these features.
A GraphQL interface defines fields that the implementing types also need to
implement. A GraphQL interface also needs to be able to determine the concrete
type name as well as downcast the general type to the actual concrete type.
## Syntax
See the documentation for [`graphql_object!`][1] on the general item and type
syntax. `graphql_interface!` requires an additional `instance_resolvers` item,
and does _not_ support the `interfaces` item.
`instance_resolvers` is a match like structure used to resolve the concrete
instance type of the interface. It starts with a context argument and continues
with a number of match arms; on the left side is the indicated type, and on the
right an expression that resolve into `Option<T>` of the type indicated:
```rust,ignore
instance_resolvers: |&context| {
&Human => context.get_human(self.id()), // returns Option<&Human>
&Droid => context.get_droid(self.id()), // returns Option<&Droid>
},
```
This is used for both the `__typename` field and when resolving a specialized
fragment, e.g. `...on Human`. For `__typename`, the resolvers will be executed
in order - the first one returning `Some` will be the determined type name. When
resolving fragment type conditions, only the corresponding match arm will be
executed.
## Example
A simplified extract from the StarWars schema example shows how to use the
shared context to implement downcasts.
```rust
# extern crate juniper;
# use std::collections::HashMap;
struct Human { id: String }
struct Droid { id: String }
struct Database {
humans: HashMap<String, Human>,
droids: HashMap<String, Droid>,
}
trait Character {
fn id(&self) -> &str;
}
impl Character for Human {
fn id(&self) -> &str { &self.id }
}
impl Character for Droid {
fn id(&self) -> &str { &self.id }
}
#[juniper::graphql_object(Context = Database)]
impl Human {
fn id(&self) -> &str { &self.id }
}
#[juniper::graphql_object(
name = "Droid",
Context = Database,
)]
impl Droid {
fn id(&self) -> &str { &self.id }
}
// You can introduce lifetimes or generic parameters by < > before the name.
juniper::graphql_interface!(<'a> &'a dyn Character: Database as "Character" |&self| {
field id() -> &str { self.id() }
instance_resolvers: |&context| {
&Human => context.humans.get(self.id()),
&Droid => context.droids.get(self.id()),
}
});
# fn main() { }
```
[1]: macro.graphql_object!.html
*/
#[macro_export]
macro_rules! graphql_interface {
(
@generate,
meta = {
lifetimes = [$($lifetimes:tt,)*],
name = $name:ty,
ctx = $ctx:ty,
main_self = $main_self:ident,
outname = {$($outname:tt)*},
scalar = {$($scalar:tt)*},
$(description = $desciption:tt,)*
additional = {
resolver = {
$(context = $resolver_ctx: ident,)*
items = [
$({
src = $resolver_src: ty,
resolver = $resolver_expr: expr,
},)*
],
},
},
},
items = [$({
name = $fn_name: ident,
body = $body: block,
return_ty = $return_ty: ty,
args = [$({
arg_name = $arg_name : ident,
arg_ty = $arg_ty: ty,
$(arg_default = $arg_default: expr,)*
$(arg_description = $arg_description: expr,)*
$(arg_docstring = $arg_docstring: expr,)*
},)*],
$(decs = $fn_description: expr,)*
$(docstring = $docstring: expr,)*
$(deprecated = $deprecated: expr,)*
$(executor_var = $executor: ident,)*
},)*],
) => {
$crate::__juniper_impl_trait!(
impl<$($scalar)* $(, $lifetimes)* > GraphQLType for $name {
fn name(_ : &Self::TypeInfo) -> Option<&'static str> {
Some($($outname)*)
}
fn meta<'r>(
info: &Self::TypeInfo,
registry: &mut $crate::Registry<'r, $crate::__juniper_insert_generic!($($scalar)+)>
) -> $crate::meta::MetaType<'r, $crate::__juniper_insert_generic!($($scalar)+)>
where
$crate::__juniper_insert_generic!($($scalar)+): 'r
{
// Ensure all child types are registered
$(
let _ = registry.get_type::<$resolver_src>(info);
)*
let fields = &[$(
registry.field_convert::<$return_ty, _, Self::Context>(
&$crate::to_camel_case(stringify!($fn_name)),
info
)
$(.description($fn_description))*
.push_docstring(&[$($docstring,)*])
$(.deprecated($deprecated))*
$(.argument(
$crate::__juniper_create_arg!(
registry = registry,
info = info,
arg_ty = $arg_ty,
arg_name = $arg_name,
$(default = $arg_default,)*
$(description = $arg_description,)*
$(docstring = $arg_docstring,)*
)
))*,
)*];
registry.build_interface_type::<$name>(
info, fields
)
$(.description($desciption))*
.into_meta()
}
}
);
$crate::__juniper_impl_trait!(
impl<$($scalar)* $(, $lifetimes)* > IsOutputType for $name { }
);
$crate::__juniper_impl_trait!(
impl<$($scalar)* $(, $lifetimes)* > GraphQLValue for $name {
type Context = $ctx;
type TypeInfo = ();
fn type_name(&self, _ : &Self::TypeInfo) -> Option<&'static str> {
Some($($outname)*)
}
#[allow(unused_variables)]
fn resolve_field(
&$main_self,
info: &Self::TypeInfo,
field: &str,
args: &$crate::Arguments<$crate::__juniper_insert_generic!($($scalar)+)>,
executor: &$crate::Executor<Self::Context, $crate::__juniper_insert_generic!($($scalar)+)>
) -> $crate::ExecutionResult<$crate::__juniper_insert_generic!($($scalar)+)> {
$(
if field == &$crate::to_camel_case(stringify!($fn_name)) {
let f = (|| {
$(
let $arg_name: $arg_ty = args.get(&$crate::to_camel_case(stringify!($arg_name)))
.expect(concat!(
"Argument ",
stringify!($arg_name),
" missing - validation must have failed"
));
)*
$(
let $executor = &executor;
)*
$body
});
let result: $return_ty = f();
return $crate::IntoResolvable::into(result, executor.context())
.and_then(|res| {
match res {
Some((ctx, r)) => {
executor.replaced_context(ctx)
.resolve_with_ctx(&(), &r)
}
None => Ok($crate::Value::null())
}
});
}
)*
panic!("Field {} not found on type {}", field, $($outname)*)
}
#[allow(unused_variables)]
fn concrete_type_name(&$main_self, context: &Self::Context, _info: &Self::TypeInfo) -> String {
$(let $resolver_ctx = &context;)*
$(
if ($resolver_expr as ::std::option::Option<$resolver_src>).is_some() {
return
<$resolver_src as $crate::GraphQLType<_>>::name(&()).unwrap().to_owned();
}
)*
panic!("Concrete type not handled by instance resolvers on {}", $($outname)*);
}
fn resolve_into_type(
&$main_self,
_info: &Self::TypeInfo,
type_name: &str,
_: Option<&[$crate::Selection<$crate::__juniper_insert_generic!($($scalar)*)>]>,
executor: &$crate::Executor<Self::Context, $crate::__juniper_insert_generic!($($scalar)*)>,
) -> $crate::ExecutionResult<$crate::__juniper_insert_generic!($($scalar)*)> {
$(let $resolver_ctx = &executor.context();)*
$(
if type_name == (<$resolver_src as $crate::GraphQLType<_>>::name(&())).unwrap() {
return executor.resolve(&(), &$resolver_expr);
}
)*
panic!("Concrete type not handled by instance resolvers on {}", $($outname)*);
}
}
);
};
(
@parse,
meta = {$($meta:tt)*},
rest = $($rest:tt)*
) => {
$crate::__juniper_parse_field_list!(
success_callback = graphql_interface,
additional_parser = {
callback = __juniper_parse_instance_resolver,
header = {},
},
meta = {$($meta)*},
items = [],
rest = $($rest)*
);
};
(@$($stuff:tt)*) => {
compile_error!("Invalid syntax for `graphql_interface!`");
};
(
$($rest:tt)*
) => {
$crate::__juniper_parse_object_header!(
callback = graphql_interface,
rest = $($rest)*
);
}
}

View file

@ -1,12 +1,6 @@
// Wrapper macros which allows built-in macros to be recognized as "crate-local"
// and helper traits for #[juniper::graphql_subscription] macro.
//! Helper definitions for macros.
#[macro_use]
mod common;
#[macro_use]
mod interface;
pub mod helper;
#[cfg(test)]
mod tests;
pub mod subscription_helpers;

View file

@ -1,15 +1,3 @@
use crate::{
ast::InputValue,
executor::FieldResult,
schema::model::RootNode,
types::scalars::{EmptyMutation, EmptySubscription},
value::{DefaultScalarValue, Object, Value},
};
struct Interface;
#[derive(Debug)]
struct Root;
/*
Syntax to validate:
@ -22,9 +10,21 @@ Syntax to validate:
*/
#[crate::graphql_object(
interfaces = [&Interface],
)]
#![allow(deprecated)]
use crate::{
ast::InputValue,
executor::FieldResult,
graphql_interface, graphql_object,
schema::model::RootNode,
types::scalars::{EmptyMutation, EmptySubscription},
value::{DefaultScalarValue, Object, Value},
};
#[derive(Debug)]
struct Root;
#[graphql_object(interfaces = [InterfaceValue])]
impl Root {
fn simple() -> i32 {
0
@ -104,44 +104,85 @@ impl Root {
}
}
graphql_interface!(Interface: () |&self| {
field simple() -> i32 { 0 }
#[graphql_interface(scalar = DefaultScalarValue)]
impl Interface for Root {
fn simple(&self) -> i32 {
0
}
field description() -> i32 as "Field description" { 0 }
fn description(&self) -> i32 {
0
}
field deprecated "Deprecation reason"
deprecated() -> i32 { 0 }
fn deprecated(&self) -> i32 {
0
}
field deprecated "Deprecation reason"
deprecated_descr() -> i32 as "Field description" { 0 }
fn deprecated_descr(&self) -> i32 {
0
}
fn attr_description(&self) -> i32 {
0
}
fn attr_description_collapse(&self) -> i32 {
0
}
fn attr_description_long(&self) -> i32 {
0
}
fn attr_deprecated(&self) -> i32 {
0
}
fn attr_deprecated_reason(&self) -> i32 {
0
}
fn attr_deprecated_descr(&self) -> i32 {
0
}
}
#[graphql_interface(for = Root, scalar = DefaultScalarValue)]
trait Interface {
fn simple(&self) -> i32;
#[graphql_interface(desc = "Field description")]
fn description(&self) -> i32;
#[graphql_interface(deprecated = "Deprecation reason")]
fn deprecated(&self) -> i32;
#[graphql_interface(desc = "Field description", deprecated = "Deprecation reason")]
fn deprecated_descr(&self) -> i32;
/// Field description
field attr_description() -> i32 { 0 }
fn attr_description(&self) -> i32;
/// Field description
/// with `collapse_docs` behavior
field attr_description_collapse() -> i32 { 0 }
fn attr_description_collapse(&self) -> i32;
/// Get the i32 representation of 0.
///
/// - This comment is longer.
/// - These two lines are rendered as bullets by GraphiQL.
field attr_description_long() -> i32 { 0 }
fn attr_description_long(&self) -> i32;
#[deprecated]
field attr_deprecated() -> i32 { 0 }
fn attr_deprecated(&self) -> i32;
#[deprecated(note = "Deprecation reason")]
field attr_deprecated_reason() -> i32 { 0 }
fn attr_deprecated_reason(&self) -> i32;
/// Field description
#[deprecated(note = "Deprecation reason")]
field attr_deprecated_descr() -> i32 { 0 }
instance_resolvers: |&_| {
Root => Some(Root {}),
}
});
fn attr_deprecated_descr(&self) -> i32;
}
async fn run_field_info_query<F>(type_name: &str, field_name: &str, f: F)
where

View file

@ -17,7 +17,7 @@ struct WithLifetime<'a> {
value: &'a str,
}
#[crate::graphql_object(Context=Context)]
#[crate::graphql_object(Context = Context)]
impl<'a> WithLifetime<'a> {
fn value(&'a self) -> &'a str {
self.value
@ -26,7 +26,7 @@ impl<'a> WithLifetime<'a> {
struct WithContext;
#[crate::graphql_object(Context=Context)]
#[crate::graphql_object(Context = Context)]
impl WithContext {
fn ctx(ctx: &Context) -> bool {
ctx.flag1

View file

@ -1,12 +1,3 @@
use std::marker::PhantomData;
use crate::{
ast::InputValue,
schema::model::RootNode,
types::scalars::{EmptyMutation, EmptySubscription},
value::{DefaultScalarValue, Object, Value},
};
/*
Syntax to validate:
@ -19,135 +10,86 @@ Syntax to validate:
*/
use crate::{
ast::InputValue,
graphql_interface, graphql_object,
schema::model::RootNode,
types::scalars::{EmptyMutation, EmptySubscription},
value::{DefaultScalarValue, Object, Value},
};
struct Concrete;
struct CustomName;
#[allow(dead_code)]
struct WithLifetime<'a> {
data: PhantomData<&'a i32>,
}
#[allow(dead_code)]
struct WithGenerics<T> {
data: T,
}
struct DescriptionFirst;
struct FieldsFirst;
struct InterfacesFirst;
struct CommasWithTrailing;
struct CommasOnMeta;
struct ResolversWithTrailingComma;
struct Root;
#[crate::graphql_object]
#[graphql_object(impl = [
CustomNameValue, DescriptionValue, WithLifetimeValue<'_>, WithGenericsValue<()>,
])]
impl Concrete {
fn simple() -> i32 {
0
}
}
graphql_interface!(CustomName: () as "ACustomNamedInterface" |&self| {
field simple() -> i32 { 0 }
#[graphql_interface(for = Concrete, name = "ACustomNamedInterface", scalar = DefaultScalarValue)]
trait CustomName {
fn simple(&self) -> i32;
}
#[graphql_interface(scalar = DefaultScalarValue)]
impl CustomName for Concrete {
fn simple(&self) -> i32 {
0
}
}
instance_resolvers: |_| { Concrete => Some(Concrete) }
});
#[graphql_interface(for = Concrete, scalar = DefaultScalarValue)]
trait WithLifetime<'a> {
fn simple(&self) -> i32;
}
#[graphql_interface(scalar = DefaultScalarValue)]
impl<'a> WithLifetime<'a> for Concrete {
fn simple(&self) -> i32 {
0
}
}
graphql_interface!(<'a> WithLifetime<'a>: () as "WithLifetime" |&self| {
field simple() -> i32 { 0 }
instance_resolvers: |_| { Concrete => Some(Concrete) }
});
#[graphql_interface(for = Concrete, scalar = DefaultScalarValue)]
trait WithGenerics<T> {
fn simple(&self) -> i32;
}
#[graphql_interface(scalar = DefaultScalarValue)]
impl<T> WithGenerics<T> for Concrete {
fn simple(&self) -> i32 {
0
}
}
graphql_interface!(<T> WithGenerics<T>: () as "WithGenerics" |&self| {
field simple() -> i32 { 0 }
instance_resolvers: |_| { Concrete => Some(Concrete) }
});
#[graphql_interface(for = Concrete, desc = "A description", scalar = DefaultScalarValue)]
trait Description {
fn simple(&self) -> i32;
}
#[graphql_interface(scalar = DefaultScalarValue)]
impl Description for Concrete {
fn simple(&self) -> i32 {
0
}
}
graphql_interface!(DescriptionFirst: () |&self| {
description: "A description"
struct Root;
field simple() -> i32 { 0 }
instance_resolvers: |_| { Concrete => Some(Concrete) }
});
graphql_interface!(FieldsFirst: () |&self| {
field simple() -> i32 { 0 }
description: "A description"
instance_resolvers: |_| { Concrete => Some(Concrete) }
});
graphql_interface!(InterfacesFirst: () |&self| {
instance_resolvers: |_| { Concrete => Some(Concrete) }
field simple() -> i32 { 0 }
description: "A description"
});
graphql_interface!(CommasWithTrailing: () |&self| {
instance_resolvers: |_| { Concrete => Some(Concrete) },
field simple() -> i32 { 0 },
description: "A description",
});
graphql_interface!(CommasOnMeta: () |&self| {
instance_resolvers: |_| { Concrete => Some(Concrete) }
description: "A description",
field simple() -> i32 { 0 }
});
graphql_interface!(ResolversWithTrailingComma: () |&self| {
instance_resolvers: |_| { Concrete => Some(Concrete), }
description: "A description",
field simple() -> i32 { 0 }
});
#[crate::graphql_object(
// FIXME: make async work
noasync
)]
impl<'a> Root {
fn custom_name() -> CustomName {
CustomName {}
#[crate::graphql_object]
impl Root {
fn custom_name() -> CustomNameValue {
Concrete.into()
}
fn with_lifetime() -> WithLifetime<'a> {
WithLifetime { data: PhantomData }
fn with_lifetime() -> WithLifetimeValue<'static> {
Concrete.into()
}
fn with_generics() -> WithGenerics<i32> {
WithGenerics { data: 123 }
fn with_generics() -> WithGenericsValue<i32> {
Concrete.into()
}
fn description_first() -> DescriptionFirst {
DescriptionFirst {}
}
fn fields_first() -> FieldsFirst {
FieldsFirst {}
}
fn interfaces_first() -> InterfacesFirst {
InterfacesFirst {}
}
fn commas_with_trailing() -> CommasWithTrailing {
CommasWithTrailing {}
}
fn commas_on_meta() -> CommasOnMeta {
CommasOnMeta {}
}
fn resolvers_with_trailing_comma() -> ResolversWithTrailingComma {
ResolversWithTrailingComma {}
fn description() -> DescriptionValue {
Concrete.into()
}
}
@ -256,115 +198,10 @@ async fn introspect_with_generics() {
#[tokio::test]
async fn introspect_description_first() {
run_type_info_query("DescriptionFirst", |object, fields| {
run_type_info_query("Description", |object, fields| {
assert_eq!(
object.get_field_value("name"),
Some(&Value::scalar("DescriptionFirst"))
);
assert_eq!(
object.get_field_value("description"),
Some(&Value::scalar("A description"))
);
assert!(fields.contains(&Value::object(
vec![("name", Value::scalar("simple"))]
.into_iter()
.collect(),
)));
})
.await;
}
#[tokio::test]
async fn introspect_fields_first() {
run_type_info_query("FieldsFirst", |object, fields| {
assert_eq!(
object.get_field_value("name"),
Some(&Value::scalar("FieldsFirst"))
);
assert_eq!(
object.get_field_value("description"),
Some(&Value::scalar("A description"))
);
assert!(fields.contains(&Value::object(
vec![("name", Value::scalar("simple"))]
.into_iter()
.collect(),
)));
})
.await;
}
#[tokio::test]
async fn introspect_interfaces_first() {
run_type_info_query("InterfacesFirst", |object, fields| {
assert_eq!(
object.get_field_value("name"),
Some(&Value::scalar("InterfacesFirst"))
);
assert_eq!(
object.get_field_value("description"),
Some(&Value::scalar("A description"))
);
assert!(fields.contains(&Value::object(
vec![("name", Value::scalar("simple"))]
.into_iter()
.collect(),
)));
})
.await;
}
#[tokio::test]
async fn introspect_commas_with_trailing() {
run_type_info_query("CommasWithTrailing", |object, fields| {
assert_eq!(
object.get_field_value("name"),
Some(&Value::scalar("CommasWithTrailing"))
);
assert_eq!(
object.get_field_value("description"),
Some(&Value::scalar("A description"))
);
assert!(fields.contains(&Value::object(
vec![("name", Value::scalar("simple"))]
.into_iter()
.collect(),
)));
})
.await;
}
#[tokio::test]
async fn introspect_commas_on_meta() {
run_type_info_query("CommasOnMeta", |object, fields| {
assert_eq!(
object.get_field_value("name"),
Some(&Value::scalar("CommasOnMeta"))
);
assert_eq!(
object.get_field_value("description"),
Some(&Value::scalar("A description"))
);
assert!(fields.contains(&Value::object(
vec![("name", Value::scalar("simple"))]
.into_iter()
.collect(),
)));
})
.await;
}
#[tokio::test]
async fn introspect_resolvers_with_trailing_comma() {
run_type_info_query("ResolversWithTrailingComma", |object, fields| {
assert_eq!(
object.get_field_value("name"),
Some(&Value::scalar("ResolversWithTrailingComma"))
Some(&Value::scalar("Description"))
);
assert_eq!(
object.get_field_value("description"),

View file

@ -639,29 +639,6 @@ impl<'a, S> Field<'a, S> {
self
}
/// Adds a (multi)line doc string to the description of the field.
/// Any leading or trailing newlines will be removed.
///
/// If the docstring contains newlines, repeated leading tab and space characters
/// will be removed from the beginning of each line.
///
/// If the description hasn't been set, the description is set to the provided line.
/// Otherwise, the doc string is added to the current description after a newline.
pub fn push_docstring(mut self, multiline: &[&str]) -> Field<'a, S> {
if let Some(docstring) = clean_docstring(multiline) {
match &mut self.description {
&mut Some(ref mut desc) => {
desc.push('\n');
desc.push_str(&docstring);
}
desc @ &mut None => {
*desc = Some(docstring);
}
}
}
self
}
/// Add an argument to the field
///
/// Arguments are unordered and can't contain duplicates by name.
@ -706,30 +683,9 @@ impl<'a, S> Argument<'a, S> {
self
}
/// Adds a (multi)line doc string to the description of the field.
/// Any leading or trailing newlines will be removed.
///
/// If the docstring contains newlines, repeated leading tab and space characters
/// will be removed from the beginning of each line.
///
/// If the description hasn't been set, the description is set to the provided line.
/// Otherwise, the doc string is added to the current description after a newline.
pub fn push_docstring(mut self, multiline: &[&str]) -> Argument<'a, S> {
if let Some(docstring) = clean_docstring(multiline) {
match &mut self.description {
&mut Some(ref mut desc) => {
desc.push('\n');
desc.push_str(&docstring);
}
desc @ &mut None => *desc = Some(docstring),
}
}
self
}
/// Set the default value of the argument
///
/// This overwrites the description if any was previously set.
/// This overwrites the default value if any was previously set.
pub fn default_value(mut self, default_value: InputValue<S>) -> Self {
self.default_value = Some(default_value);
self
@ -798,35 +754,3 @@ where
{
<T as FromInputValue<S>>::from_input_value(v).is_some()
}
fn clean_docstring(multiline: &[&str]) -> Option<String> {
if multiline.is_empty() {
return None;
}
let trim_start = multiline
.iter()
.filter_map(|ln| ln.chars().position(|ch| !ch.is_whitespace()))
.min()
.unwrap_or(0);
Some(
multiline
.iter()
.enumerate()
.flat_map(|(line, ln)| {
let new_ln = if !ln.chars().next().map(char::is_whitespace).unwrap_or(false) {
ln.trim_end() // skip trimming the first line
} else if ln.len() >= trim_start {
ln[trim_start..].trim_end()
} else {
""
};
new_ln.chars().chain(
['\n']
.iter()
.take_while(move |_| line < multiline.len() - 1)
.cloned(),
)
})
.collect::<String>(),
)
}

View file

@ -1,4 +1,3 @@
pub mod model;
pub mod schema;
#[cfg(feature = "schema-language")]
pub mod schema_language;

View file

@ -1,319 +0,0 @@
#![allow(missing_docs)]
use std::collections::HashMap;
use crate::GraphQLEnum;
#[derive(GraphQLEnum, Copy, Clone, Eq, PartialEq, Debug)]
pub enum Episode {
#[graphql(name = "NEW_HOPE")]
NewHope,
Empire,
Jedi,
}
pub trait Character {
fn id(&self) -> &str;
fn name(&self) -> &str;
fn friend_ids(&self) -> &[String];
fn appears_in(&self) -> &[Episode];
fn secret_backstory(&self) -> &Option<String>;
fn as_character(&self) -> &dyn Character;
}
pub trait Human: Character {
fn home_planet(&self) -> &Option<String>;
}
pub trait Droid: Character {
fn primary_function(&self) -> &Option<String>;
}
#[derive(Clone)]
struct HumanData {
id: String,
name: String,
friend_ids: Vec<String>,
appears_in: Vec<Episode>,
secret_backstory: Option<String>,
home_planet: Option<String>,
}
#[derive(Clone)]
struct DroidData {
id: String,
name: String,
friend_ids: Vec<String>,
appears_in: Vec<Episode>,
secret_backstory: Option<String>,
primary_function: Option<String>,
}
impl Character for HumanData {
fn id(&self) -> &str {
&self.id
}
fn name(&self) -> &str {
&self.name
}
fn friend_ids(&self) -> &[String] {
&self.friend_ids
}
fn appears_in(&self) -> &[Episode] {
&self.appears_in
}
fn secret_backstory(&self) -> &Option<String> {
&self.secret_backstory
}
fn as_character(&self) -> &dyn Character {
self
}
}
impl Human for HumanData {
fn home_planet(&self) -> &Option<String> {
&self.home_planet
}
}
impl Character for DroidData {
fn id(&self) -> &str {
&self.id
}
fn name(&self) -> &str {
&self.name
}
fn friend_ids(&self) -> &[String] {
&self.friend_ids
}
fn appears_in(&self) -> &[Episode] {
&self.appears_in
}
fn secret_backstory(&self) -> &Option<String> {
&self.secret_backstory
}
fn as_character(&self) -> &dyn Character {
self
}
}
impl Droid for DroidData {
fn primary_function(&self) -> &Option<String> {
&self.primary_function
}
}
#[derive(Default, Clone)]
pub struct Database {
humans: HashMap<String, HumanData>,
droids: HashMap<String, DroidData>,
}
use crate::{
executor::Registry,
schema::meta::MetaType,
types::base::{GraphQLType, GraphQLValue},
value::ScalarValue,
};
impl<S> GraphQLType<S> for Database
where
S: ScalarValue,
{
fn name(_: &()) -> Option<&str> {
Some("_Database")
}
fn meta<'r>(_: &(), registry: &mut Registry<'r, S>) -> MetaType<'r, S>
where
S: 'r,
{
registry.build_object_type::<Self>(&(), &[]).into_meta()
}
}
impl<S> GraphQLValue<S> for Database
where
S: ScalarValue,
{
type Context = Self;
type TypeInfo = ();
fn type_name<'i>(&self, info: &'i Self::TypeInfo) -> Option<&'i str> {
<Self as GraphQLType<S>>::name(info)
}
}
impl HumanData {
pub fn new(
id: &str,
name: &str,
friend_ids: &[&str],
appears_in: &[Episode],
secret_backstory: Option<&str>,
home_planet: Option<&str>,
) -> HumanData {
HumanData {
id: id.to_owned(),
name: name.to_owned(),
friend_ids: friend_ids
.to_owned()
.into_iter()
.map(|f| f.to_owned())
.collect(),
appears_in: appears_in.to_vec(),
secret_backstory: secret_backstory.map(|b| b.to_owned()),
home_planet: home_planet.map(|p| p.to_owned()),
}
}
}
impl DroidData {
pub fn new(
id: &str,
name: &str,
friend_ids: &[&str],
appears_in: &[Episode],
secret_backstory: Option<&str>,
primary_function: Option<&str>,
) -> DroidData {
DroidData {
id: id.to_owned(),
name: name.to_owned(),
friend_ids: friend_ids
.to_owned()
.into_iter()
.map(|f| f.to_owned())
.collect(),
appears_in: appears_in.to_vec(),
secret_backstory: secret_backstory.map(|b| b.to_owned()),
primary_function: primary_function.map(|p| p.to_owned()),
}
}
}
impl Database {
pub fn new() -> Database {
let mut humans = HashMap::new();
let mut droids = HashMap::new();
humans.insert(
"1000".to_owned(),
HumanData::new(
"1000",
"Luke Skywalker",
&["1002", "1003", "2000", "2001"],
&[Episode::NewHope, Episode::Empire, Episode::Jedi],
None,
Some("Tatooine"),
),
);
humans.insert(
"1001".to_owned(),
HumanData::new(
"1001",
"Darth Vader",
&["1004"],
&[Episode::NewHope, Episode::Empire, Episode::Jedi],
None,
Some("Tatooine"),
),
);
humans.insert(
"1002".to_owned(),
HumanData::new(
"1002",
"Han Solo",
&["1000", "1003", "2001"],
&[Episode::NewHope, Episode::Empire, Episode::Jedi],
None,
None,
),
);
humans.insert(
"1003".to_owned(),
HumanData::new(
"1003",
"Leia Organa",
&["1000", "1002", "2000", "2001"],
&[Episode::NewHope, Episode::Empire, Episode::Jedi],
None,
Some("Alderaan"),
),
);
humans.insert(
"1004".to_owned(),
HumanData::new(
"1004",
"Wilhuff Tarkin",
&["1001"],
&[Episode::NewHope],
None,
None,
),
);
droids.insert(
"2000".to_owned(),
DroidData::new(
"2000",
"C-3PO",
&["1000", "1002", "1003", "2001"],
&[Episode::NewHope, Episode::Empire, Episode::Jedi],
None,
Some("Protocol"),
),
);
droids.insert(
"2001".to_owned(),
DroidData::new(
"2001",
"R2-D2",
&["1000", "1002", "1003"],
&[Episode::NewHope, Episode::Empire, Episode::Jedi],
None,
Some("Astromech"),
),
);
Database { humans, droids }
}
pub fn get_hero(&self, episode: Option<Episode>) -> &dyn Character {
if episode == Some(Episode::Empire) {
self.get_human("1000").unwrap().as_character()
} else {
self.get_droid("2001").unwrap().as_character()
}
}
pub fn get_human(&self, id: &str) -> Option<&dyn Human> {
self.humans.get(id).map(|h| h as &dyn Human)
}
pub fn get_droid(&self, id: &str) -> Option<&dyn Droid> {
self.droids.get(id).map(|d| d as &dyn Droid)
}
pub fn get_character(&self, id: &str) -> Option<&dyn Character> {
if let Some(h) = self.humans.get(id) {
Some(h)
} else if let Some(d) = self.droids.get(id) {
Some(d)
} else {
None
}
}
pub fn get_friends(&self, c: &dyn Character) -> Vec<&dyn Character> {
c.friend_ids()
.iter()
.flat_map(|id| self.get_character(id))
.collect()
}
}

View file

@ -1,164 +1,385 @@
#![allow(missing_docs)]
use std::{collections::HashMap, pin::Pin};
use crate::{
executor::Context,
graphql_subscription,
tests::fixtures::starwars::model::{Character, Database, Droid, Episode, Human},
GraphQLObject,
graphql_interface, graphql_object, graphql_subscription, Context, DefaultScalarValue,
GraphQLEnum,
};
use std::pin::Pin;
impl Context for Database {}
graphql_interface!(<'a> &'a dyn Character: Database as "Character" |&self| {
description: "A character in the Star Wars Trilogy"
field id() -> &str as "The id of the character" {
self.id()
}
field name() -> Option<&str> as "The name of the character" {
Some(self.name())
}
field friends(&executor) -> Vec<&dyn Character>
as "The friends of the character" {
executor.context().get_friends(self.as_character())
}
field appears_in() -> &[Episode] as "Which movies they appear in" {
self.appears_in()
}
instance_resolvers: |&context| {
&dyn Human => context.get_human(&self.id()),
&dyn Droid => context.get_droid(&self.id()),
}
});
#[crate::graphql_object(
Context = Database,
Scalar = crate::DefaultScalarValue,
interfaces = [&dyn Character],
// FIXME: make async work
noasync
)]
/// A humanoid creature in the Star Wars universe.
impl<'a> &'a dyn Human {
/// The id of the human
fn id(&self) -> &str {
self.id()
}
/// The name of the human
fn name(&self) -> Option<&str> {
Some(self.name())
}
/// The friends of the human
fn friends(&self, ctx: &Database) -> Vec<&dyn Character> {
ctx.get_friends(self.as_character())
}
/// Which movies they appear in
fn appears_in(&self) -> &[Episode] {
self.appears_in()
}
/// The home planet of the human
fn home_planet(&self) -> &Option<String> {
self.home_planet()
}
}
#[crate::graphql_object(
Context = Database,
Scalar = crate::DefaultScalarValue,
interfaces = [&dyn Character],
// FIXME: make async work
noasync
)]
/// A mechanical creature in the Star Wars universe.
impl<'a> &'a dyn Droid {
/// The id of the droid
fn id(&self) -> &str {
self.id()
}
/// The name of the droid
fn name(&self) -> Option<&str> {
Some(self.name())
}
/// The friends of the droid
fn friends(&self, ctx: &Database) -> Vec<&dyn Character> {
ctx.get_friends(self.as_character())
}
/// Which movies they appear in
fn appears_in(&self) -> &[Episode] {
self.appears_in()
}
/// The primary function of the droid
fn primary_function(&self) -> &Option<String> {
self.primary_function()
}
}
pub struct Query;
#[crate::graphql_object(
Context = Database,
Scalar = crate::DefaultScalarValue,
// FIXME: make async work
noasync
)]
#[graphql_object(context = Database, scalar = DefaultScalarValue)]
/// The root query object of the schema
impl Query {
#[graphql(arguments(id(description = "id of the human")))]
fn human(database: &Database, id: String) -> Option<&dyn Human> {
fn human(database: &Database, id: String) -> Option<&Human> {
database.get_human(&id)
}
#[graphql(arguments(id(description = "id of the droid")))]
fn droid(database: &Database, id: String) -> Option<&dyn Droid> {
fn droid(database: &Database, id: String) -> Option<&Droid> {
database.get_droid(&id)
}
#[graphql(arguments(episode(
description = "If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode"
description = "If omitted, returns the hero of the whole saga. \
If provided, returns the hero of that particular episode"
)))]
fn hero(database: &Database, episode: Option<Episode>) -> Option<&dyn Character> {
Some(database.get_hero(episode).as_character())
fn hero(database: &Database, episode: Option<Episode>) -> Option<CharacterValue> {
Some(database.get_hero(episode))
}
}
#[derive(GraphQLObject)]
#[graphql(description = "A humanoid creature in the Star Wars universe")]
#[derive(Clone)]
/// A humanoid creature in the Star Wars universe.
/// TODO: remove this when async interfaces are merged
struct HumanSubscription {
id: String,
name: String,
home_planet: String,
}
pub struct Subscription;
type HumanStream = Pin<Box<dyn futures::Stream<Item = HumanSubscription> + Send>>;
type HumanStream = Pin<Box<dyn futures::Stream<Item = Human> + Send>>;
#[graphql_subscription(context = Database)]
/// Super basic subscription fixture
impl Subscription {
async fn async_human() -> HumanStream {
Box::pin(futures::stream::once(async {
HumanSubscription {
id: "stream id".to_string(),
name: "stream name".to_string(),
home_planet: "stream home planet".to_string(),
}
}))
async fn async_human(context: &Database) -> HumanStream {
let human = context.get_human("1000").unwrap().clone();
Box::pin(futures::stream::once(futures::future::ready(human)))
}
}
#[derive(GraphQLEnum, Clone, Copy, Debug, Eq, PartialEq)]
pub enum Episode {
#[graphql(name = "NEW_HOPE")]
NewHope,
Empire,
Jedi,
}
#[graphql_interface(for = [Human, Droid], context = Database, scalar = DefaultScalarValue)]
/// A character in the Star Wars Trilogy
pub trait Character {
/// The id of the character
fn id(&self) -> &str;
/// The name of the character
fn name(&self) -> Option<&str>;
/// The friends of the character
fn friends(&self, ctx: &Database) -> Vec<CharacterValue>;
/// Which movies they appear in
fn appears_in(&self) -> &[Episode];
#[graphql_interface(ignore)]
fn friends_ids(&self) -> &[String];
}
#[derive(Clone)]
pub struct Human {
id: String,
name: String,
friend_ids: Vec<String>,
appears_in: Vec<Episode>,
secret_backstory: Option<String>,
home_planet: Option<String>,
}
impl Human {
pub fn new(
id: &str,
name: &str,
friend_ids: &[&str],
appears_in: &[Episode],
secret_backstory: Option<&str>,
home_planet: Option<&str>,
) -> Self {
Self {
id: id.to_owned(),
name: name.to_owned(),
friend_ids: friend_ids
.to_owned()
.into_iter()
.map(ToOwned::to_owned)
.collect(),
appears_in: appears_in.to_vec(),
secret_backstory: secret_backstory.map(ToOwned::to_owned),
home_planet: home_planet.map(|p| p.to_owned()),
}
}
}
/// A humanoid creature in the Star Wars universe.
#[graphql_object(
context = Database,
scalar = DefaultScalarValue,
interfaces = CharacterValue,
)]
impl Human {
/// The id of the human
fn id(&self) -> &str {
&self.id
}
/// The name of the human
fn name(&self) -> Option<&str> {
Some(self.name.as_str())
}
/// The friends of the human
fn friends(&self, ctx: &Database) -> Vec<CharacterValue> {
ctx.get_friends(self)
}
/// Which movies they appear in
fn appears_in(&self) -> &[Episode] {
&self.appears_in
}
/// The home planet of the human
fn home_planet(&self) -> &Option<String> {
&self.home_planet
}
}
#[graphql_interface(scalar = DefaultScalarValue)]
impl Character for Human {
fn id(&self) -> &str {
&self.id
}
fn name(&self) -> Option<&str> {
Some(&self.name)
}
fn friends(&self, ctx: &Database) -> Vec<CharacterValue> {
ctx.get_friends(self)
}
fn appears_in(&self) -> &[Episode] {
&self.appears_in
}
fn friends_ids(&self) -> &[String] {
&self.friend_ids
}
}
#[derive(Clone)]
pub struct Droid {
id: String,
name: String,
friend_ids: Vec<String>,
appears_in: Vec<Episode>,
secret_backstory: Option<String>,
primary_function: Option<String>,
}
impl Droid {
pub fn new(
id: &str,
name: &str,
friend_ids: &[&str],
appears_in: &[Episode],
secret_backstory: Option<&str>,
primary_function: Option<&str>,
) -> Self {
Self {
id: id.to_owned(),
name: name.to_owned(),
friend_ids: friend_ids
.to_owned()
.into_iter()
.map(ToOwned::to_owned)
.collect(),
appears_in: appears_in.to_vec(),
secret_backstory: secret_backstory.map(ToOwned::to_owned),
primary_function: primary_function.map(ToOwned::to_owned),
}
}
}
/// A mechanical creature in the Star Wars universe.
#[graphql_object(
context = Database,
scalar = DefaultScalarValue,
interfaces = CharacterValue,
)]
impl Droid {
/// The id of the droid
fn id(&self) -> &str {
&self.id
}
/// The name of the droid
fn name(&self) -> Option<&str> {
Some(self.name.as_str())
}
/// The friends of the droid
fn friends(&self, ctx: &Database) -> Vec<CharacterValue> {
ctx.get_friends(self)
}
/// Which movies they appear in
fn appears_in(&self) -> &[Episode] {
&self.appears_in
}
/// The primary function of the droid
fn primary_function(&self) -> &Option<String> {
&self.primary_function
}
}
#[graphql_interface(scalar = DefaultScalarValue)]
impl Character for Droid {
fn id(&self) -> &str {
&self.id
}
fn name(&self) -> Option<&str> {
Some(&self.name)
}
fn friends(&self, ctx: &Database) -> Vec<CharacterValue> {
ctx.get_friends(self)
}
fn appears_in(&self) -> &[Episode] {
&self.appears_in
}
fn friends_ids(&self) -> &[String] {
&self.friend_ids
}
}
#[derive(Default, Clone)]
pub struct Database {
humans: HashMap<String, Human>,
droids: HashMap<String, Droid>,
}
impl Context for Database {}
impl Database {
pub fn new() -> Database {
let mut humans = HashMap::new();
let mut droids = HashMap::new();
humans.insert(
"1000".to_owned(),
Human::new(
"1000",
"Luke Skywalker",
&["1002", "1003", "2000", "2001"],
&[Episode::NewHope, Episode::Empire, Episode::Jedi],
None,
Some("Tatooine"),
),
);
humans.insert(
"1001".to_owned(),
Human::new(
"1001",
"Darth Vader",
&["1004"],
&[Episode::NewHope, Episode::Empire, Episode::Jedi],
None,
Some("Tatooine"),
),
);
humans.insert(
"1002".to_owned(),
Human::new(
"1002",
"Han Solo",
&["1000", "1003", "2001"],
&[Episode::NewHope, Episode::Empire, Episode::Jedi],
None,
None,
),
);
humans.insert(
"1003".to_owned(),
Human::new(
"1003",
"Leia Organa",
&["1000", "1002", "2000", "2001"],
&[Episode::NewHope, Episode::Empire, Episode::Jedi],
None,
Some("Alderaan"),
),
);
humans.insert(
"1004".to_owned(),
Human::new(
"1004",
"Wilhuff Tarkin",
&["1001"],
&[Episode::NewHope],
None,
None,
),
);
droids.insert(
"2000".to_owned(),
Droid::new(
"2000",
"C-3PO",
&["1000", "1002", "1003", "2001"],
&[Episode::NewHope, Episode::Empire, Episode::Jedi],
None,
Some("Protocol"),
),
);
droids.insert(
"2001".to_owned(),
Droid::new(
"2001",
"R2-D2",
&["1000", "1002", "1003"],
&[Episode::NewHope, Episode::Empire, Episode::Jedi],
None,
Some("Astromech"),
),
);
Database { humans, droids }
}
pub fn get_hero(&self, episode: Option<Episode>) -> CharacterValue {
if episode == Some(Episode::Empire) {
self.get_human("1000").unwrap().clone().into()
} else {
self.get_droid("2001").unwrap().clone().into()
}
}
pub fn get_human(&self, id: &str) -> Option<&Human> {
self.humans.get(id)
}
pub fn get_droid(&self, id: &str) -> Option<&Droid> {
self.droids.get(id)
}
pub fn get_character(&self, id: &str) -> Option<CharacterValue> {
if let Some(h) = self.humans.get(id) {
Some(h.clone().into())
} else if let Some(d) = self.droids.get(id) {
Some(d.clone().into())
} else {
None
}
}
pub fn get_friends(&self, c: &dyn Character) -> Vec<CharacterValue> {
c.friends_ids()
.iter()
.flat_map(|id| self.get_character(id))
.collect()
}
}

View file

@ -8,7 +8,8 @@ mod tests {
use crate::{
schema::model::RootNode,
tests::fixtures::starwars::{
model::Database, schema::Query, schema_language::STATIC_GRAPHQL_SCHEMA_DEFINITION,
schema::{Database, Query},
schema_language::STATIC_GRAPHQL_SCHEMA_DEFINITION,
},
types::scalars::{EmptyMutation, EmptySubscription},
};

View file

@ -1,14 +1,15 @@
use std::collections::HashSet;
use super::schema_introspection::*;
use crate::{
executor::Variables,
introspection::IntrospectionFormat,
schema::model::RootNode,
tests::fixtures::starwars::{model::Database, schema::Query},
tests::fixtures::starwars::schema::{Database, Query},
types::scalars::{EmptyMutation, EmptySubscription},
};
use super::schema_introspection::*;
#[tokio::test]
async fn test_introspection_query_type_name() {
let doc = r#"

View file

@ -2,7 +2,7 @@ use crate::{
ast::InputValue,
executor::Variables,
schema::model::RootNode,
tests::fixtures::starwars::{model::Database, schema::Query},
tests::fixtures::starwars::schema::{Database, Query},
types::scalars::{EmptyMutation, EmptySubscription},
value::Value,
};
@ -641,8 +641,8 @@ async fn test_query_inline_fragments_droid() {
"hero",
Value::object(
vec![
("name", Value::scalar("R2-D2")),
("__typename", Value::scalar("Droid")),
("name", Value::scalar("R2-D2")),
("primaryFunction", Value::scalar("Astromech")),
]
.into_iter()
@ -662,8 +662,8 @@ async fn test_query_inline_fragments_human() {
let doc = r#"
query InlineFragments {
hero(episode: EMPIRE) {
name
__typename
name
}
}
"#;
@ -682,8 +682,8 @@ async fn test_query_inline_fragments_human() {
"hero",
Value::object(
vec![
("name", Value::scalar("Luke Skywalker")),
("__typename", Value::scalar("Human")),
("name", Value::scalar("Luke Skywalker")),
]
.into_iter()
.collect(),

View file

@ -113,9 +113,15 @@ where
crate::sa::assert_obj_safe!(GraphQLValueAsync<Context = (), TypeInfo = ()>);
/// Helper alias for naming [trait objects][1] of [`GraphQLValueAsync`].
///
/// [1]: https://doc.rust-lang.org/reference/types/trait-object.html
pub type DynGraphQLValueAsync<S, C, TI> =
dyn GraphQLValueAsync<S, Context = C, TypeInfo = TI> + Send + 'static;
/// Extension of [`GraphQLType`] trait with asynchronous queries/mutations resolvers.
///
/// It's automatically implemented for [`GraphQLValueAsync`] and [`GraphQLType`] implementors, so
/// It's automatically implemented for [`GraphQLValueAsync`] and [`GraphQLType`] implementers, so
/// doesn't require manual or code-generated implementation.
pub trait GraphQLTypeAsync<S = DefaultScalarValue>: GraphQLValueAsync<S> + GraphQLType<S>
where

View file

@ -290,6 +290,12 @@ where
crate::sa::assert_obj_safe!(GraphQLValue<Context = (), TypeInfo = ()>);
/// Helper alias for naming [trait objects][1] of [`GraphQLValue`].
///
/// [1]: https://doc.rust-lang.org/reference/types/trait-object.html
pub type DynGraphQLValue<S, C, TI> =
dyn GraphQLValue<S, Context = C, TypeInfo = TI> + Send + Sync + 'static;
/// Primary trait used to expose Rust types in a GraphQL schema.
///
/// All of the convenience macros ultimately expand into an implementation of

View file

@ -23,13 +23,37 @@ pub trait GraphQLObjectType<S: ScalarValue>: GraphQLType<S> {
fn mark() {}
}
/// Maker trait for [GraphQL interfaces][1].
///
/// This trait extends the [`GraphQLType`] and is only used to mark an [interface][1]. During
/// compile this addition information is required to prevent unwanted structure compiling. If an
/// object requires this trait instead of the [`GraphQLType`], then it explicitly requires
/// [GraphQL interfaces][1]. Other types ([scalars][2], [enums][3], [objects][4], [input objects][5]
/// and [unions][6]) are not allowed.
///
/// [1]: https://spec.graphql.org/June2018/#sec-Interfaces
/// [2]: https://spec.graphql.org/June2018/#sec-Scalars
/// [3]: https://spec.graphql.org/June2018/#sec-Enums
/// [4]: https://spec.graphql.org/June2018/#sec-Objects
/// [5]: https://spec.graphql.org/June2018/#sec-Input-Objects
/// [6]: https://spec.graphql.org/June2018/#sec-Unions
pub trait GraphQLInterface<S: ScalarValue>: GraphQLType<S> {
/// An arbitrary function without meaning.
///
/// May contain compile timed check logic which ensures that types are used correctly according
/// to the [GraphQL specification][1].
///
/// [1]: https://spec.graphql.org/June2018/
fn mark() {}
}
/// Maker trait for [GraphQL unions][1].
///
/// This trait extends the [`GraphQLType`] and is only used to mark [union][1]. During compile this
/// addition information is required to prevent unwanted structure compiling. If an object requires
/// this trait instead of the [`GraphQLType`], then it explicitly requires [GraphQL unions][1].
/// Other types ([scalars][2], [enums][3], [objects][4], [input objects][5] and [interfaces][6]) are
/// not allowed.
/// This trait extends the [`GraphQLType`] and is only used to mark an [union][1]. During compile
/// this addition information is required to prevent unwanted structure compiling. If an object
/// requires this trait instead of the [`GraphQLType`], then it explicitly requires
/// [GraphQL unions][1]. Other types ([scalars][2], [enums][3], [objects][4], [input objects][5] and
/// [interfaces][6]) are not allowed.
///
/// [1]: https://spec.graphql.org/June2018/#sec-Unions
/// [2]: https://spec.graphql.org/June2018/#sec-Scalars
@ -97,7 +121,7 @@ where
{
}
impl<'a, S, T> IsOutputType<S> for &'a [T]
impl<S, T> IsOutputType<S> for [T]
where
T: IsOutputType<S>,
S: ScalarValue,
@ -111,7 +135,7 @@ where
{
}
impl<'a, S, T> IsInputType<S> for &'a [T]
impl<S, T> IsInputType<S> for [T]
where
T: IsInputType<S>,
S: ScalarValue,
@ -120,13 +144,13 @@ where
impl<'a, S, T> IsInputType<S> for &T
where
T: IsInputType<S>,
T: IsInputType<S> + ?Sized,
S: ScalarValue,
{
}
impl<'a, S, T> IsOutputType<S> for &T
where
T: IsOutputType<S>,
T: IsOutputType<S> + ?Sized,
S: ScalarValue,
{
}
@ -144,5 +168,5 @@ where
{
}
impl<'a, S> IsInputType<S> for &str where S: ScalarValue {}
impl<'a, S> IsOutputType<S> for &str where S: ScalarValue {}
impl<'a, S> IsInputType<S> for str where S: ScalarValue {}
impl<'a, S> IsOutputType<S> for str where S: ScalarValue {}

View file

@ -201,7 +201,7 @@ crate::sa::assert_obj_safe!(GraphQLSubscriptionValue<Context = (), TypeInfo = ()
/// Extension of [`GraphQLType`] trait with asynchronous [subscription][1] execution logic.
///
/// It's automatically implemented for [`GraphQLSubscriptionValue`] and [`GraphQLType`]
/// implementors, so doesn't require manual or code-generated implementation.
/// implementers, so doesn't require manual or code-generated implementation.
///
/// [1]: https://spec.graphql.org/June2018/#sec-Subscription
pub trait GraphQLSubscriptionType<S = DefaultScalarValue>:

View file

@ -5,7 +5,7 @@ use std::env;
use actix_cors::Cors;
use actix_web::{middleware, web, App, Error, HttpResponse, HttpServer};
use juniper::{
tests::fixtures::starwars::{model::Database, schema::Query},
tests::fixtures::starwars::schema::{Database, Query},
EmptyMutation, EmptySubscription, RootNode,
};
use juniper_actix::{

View file

@ -489,7 +489,7 @@ mod tests {
use juniper::{
futures::stream::StreamExt,
http::tests::{run_http_test_suite, HttpIntegration, TestResponse},
tests::fixtures::starwars::{model::Database, schema::Query},
tests::fixtures::starwars::schema::{Database, Query},
EmptyMutation, EmptySubscription, RootNode,
};
@ -666,7 +666,7 @@ mod tests {
#[actix_web::rt::test]
async fn batch_request_works() {
use juniper::{
tests::fixtures::starwars::{model::Database, schema::Query},
tests::fixtures::starwars::schema::{Database, Query},
EmptyMutation, EmptySubscription, RootNode,
};

View file

@ -0,0 +1,47 @@
//! Common code generated parts, used by this crate.
use proc_macro2::TokenStream;
use quote::quote;
/// Generate the code resolving some [GraphQL type][1] in a synchronous manner.
///
/// Value of a [GraphQL type][1] should be stored in a `res` binding in the generated code, before
/// including this piece of code.
///
/// [1]: https://spec.graphql.org/June2018/#sec-Types
pub(crate) fn sync_resolving_code() -> TokenStream {
quote! {
::juniper::IntoResolvable::into(res, executor.context())
.and_then(|res| match res {
Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(info, &r),
None => Ok(::juniper::Value::null()),
})
}
}
/// Generate the code resolving some [GraphQL type][1] in an asynchronous manner.
///
/// Value of a [GraphQL type][1] should be resolvable with `fut` binding representing a [`Future`]
/// in the generated code, before including this piece of code.
///
/// Optional `ty` argument may be used to annotate a concrete type of the resolving
/// [GraphQL type][1] (the [`Future::Output`]).
///
/// [`Future`]: std::future::Future
/// [`Future::Output`]: std::future::Future::Output
/// [1]: https://spec.graphql.org/June2018/#sec-Types
pub(crate) fn async_resolving_code(ty: Option<&syn::Type>) -> TokenStream {
let ty = ty.map(|t| quote! { : #t });
quote! {
Box::pin(::juniper::futures::FutureExt::then(fut, move |res #ty| async move {
match ::juniper::IntoResolvable::into(res, executor.context())? {
Some((ctx, r)) => {
let subexec = executor.replaced_context(ctx);
subexec.resolve_with_ctx_async(info, &r).await
},
None => Ok(::juniper::Value::null()),
}
}))
}
}

View file

@ -0,0 +1,89 @@
//! Common functions, definitions and extensions for code generation, used by this crate.
pub(crate) mod gen;
pub(crate) mod parse;
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::parse_quote;
/// [`ScalarValue`] parametrization of the code generation.
///
/// [`ScalarValue`]: juniper::ScalarValue
#[derive(Clone, Debug)]
pub(crate) enum ScalarValueType {
/// Concrete Rust type is specified as [`ScalarValue`].
///
/// [`ScalarValue`]: juniper::ScalarValue
Concrete(syn::Type),
/// One of type parameters of the original type is specified as [`ScalarValue`].
///
/// The original type is the type that the code is generated for.
///
/// [`ScalarValue`]: juniper::ScalarValue
ExplicitGeneric(syn::Ident),
/// [`ScalarValue`] parametrization is assumed to be a generic and is not specified explicitly.
///
/// [`ScalarValue`]: juniper::ScalarValue
ImplicitGeneric,
}
impl ScalarValueType {
/// Indicates whether this [`ScalarValueType`] is generic.
#[must_use]
pub(crate) fn is_generic(&self) -> bool {
matches!(self, Self::ExplicitGeneric(_) | Self::ImplicitGeneric)
}
/// Indicates whether this [`ScalarValueType`] is [`ScalarValueType::ExplicitGeneric`].
#[must_use]
pub(crate) fn is_explicit_generic(&self) -> bool {
matches!(self, Self::ExplicitGeneric(_))
}
/// Indicates whether this [`ScalarValueType`] is [`ScalarValueType::ImplicitGeneric`].
#[must_use]
pub(crate) fn is_implicit_generic(&self) -> bool {
matches!(self, Self::ImplicitGeneric)
}
/// Returns a type identifier which represents this [`ScalarValueType`].
#[must_use]
pub(crate) fn ty(&self) -> syn::Type {
match self {
Self::Concrete(ty) => ty.clone(),
Self::ExplicitGeneric(ty_param) => parse_quote! { #ty_param },
Self::ImplicitGeneric => parse_quote! { __S },
}
}
/// Returns a type parameter identifier that suits this [`ScalarValueType`].
#[must_use]
pub(crate) fn generic_ty(&self) -> syn::Type {
match self {
Self::ExplicitGeneric(ty_param) => parse_quote! { #ty_param },
Self::ImplicitGeneric | Self::Concrete(_) => parse_quote! { __S },
}
}
/// Returns a default [`ScalarValue`] type that is compatible with this [`ScalarValueType`].
///
/// [`ScalarValue`]: juniper::ScalarValue
#[must_use]
pub(crate) fn default_ty(&self) -> syn::Type {
match self {
Self::Concrete(ty) => ty.clone(),
Self::ExplicitGeneric(_) | Self::ImplicitGeneric => {
parse_quote! { ::juniper::DefaultScalarValue }
}
}
}
}
impl ToTokens for ScalarValueType {
fn to_tokens(&self, into: &mut TokenStream) {
self.ty().to_tokens(into)
}
}

View file

@ -0,0 +1,101 @@
//! Common functions, definitions and extensions for parsing and modifying Rust attributes, used by
//! this crate.
use proc_macro2::{Span, TokenStream};
use syn::parse_quote;
use crate::util::path_eq_single;
/// Prepends the given `attrs` collection with a new [`syn::Attribute`] generated from the given
/// `attr_path` and `attr_args`.
///
/// This function is generally used for uniting `proc_macro_attribute` with its body attributes.
pub(crate) fn unite(
(attr_path, attr_args): (&str, &TokenStream),
attrs: &[syn::Attribute],
) -> Vec<syn::Attribute> {
let mut full_attrs = Vec::with_capacity(attrs.len() + 1);
let attr_path = syn::Ident::new(attr_path, Span::call_site());
full_attrs.push(parse_quote! { #[#attr_path(#attr_args)] });
full_attrs.extend_from_slice(attrs);
full_attrs
}
/// Strips all `attr_path` attributes from the given `attrs` collection.
///
/// This function is generally used for removing duplicate attributes during `proc_macro_attribute`
/// expansion, so avoid unnecessary expansion duplication.
pub(crate) fn strip(attr_path: &str, attrs: Vec<syn::Attribute>) -> Vec<syn::Attribute> {
attrs
.into_iter()
.filter(|attr| !path_eq_single(&attr.path, attr_path))
.collect()
}
/// Common errors of parsing Rust attributes, appeared in this crate.
pub(crate) mod err {
use proc_macro2::Span;
use syn::spanned::Spanned;
/// Creates "duplicated argument" [`syn::Error`] for the given `name` pointing to the given
/// `span`.
#[must_use]
pub(crate) fn dup_arg<S: AsSpan>(span: S) -> syn::Error {
syn::Error::new(span.as_span(), "duplicated attribute argument found")
}
/// Creates "unknown argument" [`syn::Error`] for the given `name` pointing to the given `span`.
#[must_use]
pub(crate) fn unknown_arg<S: AsSpan>(span: S, name: &str) -> syn::Error {
syn::Error::new(
span.as_span(),
format!("unknown `{}` attribute argument", name),
)
}
/// Helper coercion for [`Span`] and [`Spanned`] types to use in function arguments.
pub(crate) trait AsSpan {
/// Returns the coerced [`Span`].
#[must_use]
fn as_span(&self) -> Span;
}
impl AsSpan for Span {
#[inline]
fn as_span(&self) -> Self {
*self
}
}
impl<T: Spanned> AsSpan for &T {
#[inline]
fn as_span(&self) -> Span {
self.span()
}
}
}
/// Handy extension of [`Option`] methods, used in this crate.
pub(crate) trait OptionExt {
type Inner;
/// Transforms the `Option<T>` into a `Result<(), E>`, mapping `None` to `Ok(())` and `Some(v)`
/// to `Err(err(v))`.
fn none_or_else<E, F>(self, err: F) -> Result<(), E>
where
F: FnOnce(Self::Inner) -> E;
}
impl<T> OptionExt for Option<T> {
type Inner = T;
fn none_or_else<E, F>(self, err: F) -> Result<(), E>
where
F: FnOnce(T) -> E,
{
match self {
Some(v) => Err(err(v)),
None => Ok(()),
}
}
}

View file

@ -0,0 +1,94 @@
//! Common functions, definitions and extensions for parsing downcasting functions, used by GraphQL
//! [interfaces][1] and [unions][2] definitions to downcast its type to a concrete implementer type.
//!
//! [1]: https://spec.graphql.org/June2018/#sec-Interfaces
//! [2]: https://spec.graphql.org/June2018/#sec-Unions
use proc_macro2::Span;
use syn::{ext::IdentExt as _, spanned::Spanned as _};
use crate::common::parse::TypeExt as _;
/// Parses downcasting output type from the downcaster method return type.
///
/// # Errors
///
/// If return type is invalid (not `Option<&OutputType>`), then returns the [`Span`] to display the
/// corresponding error at.
pub(crate) fn output_type(ret_ty: &syn::ReturnType) -> Result<syn::Type, Span> {
let ret_ty = match &ret_ty {
syn::ReturnType::Type(_, ty) => &*ty,
_ => return Err(ret_ty.span()),
};
let path = match ret_ty.unparenthesized() {
syn::Type::Path(syn::TypePath { qself: None, path }) => path,
_ => return Err(ret_ty.span()),
};
let (ident, args) = match path.segments.last() {
Some(syn::PathSegment {
ident,
arguments: syn::PathArguments::AngleBracketed(generic),
}) => (ident, &generic.args),
_ => return Err(ret_ty.span()),
};
if ident.unraw() != "Option" {
return Err(ret_ty.span());
}
if args.len() != 1 {
return Err(ret_ty.span());
}
let out_ty = match args.first() {
Some(syn::GenericArgument::Type(inner_ty)) => match inner_ty.unparenthesized() {
syn::Type::Reference(inner_ty) => {
if inner_ty.mutability.is_some() {
return Err(inner_ty.span());
}
inner_ty.elem.unparenthesized().clone()
}
_ => return Err(ret_ty.span()),
},
_ => return Err(ret_ty.span()),
};
Ok(out_ty)
}
/// Parses context type used for downcasting from the downcaster method signature.
///
/// Returns [`None`] if downcaster method doesn't accept context.
///
/// # Errors
///
/// If input arguments are invalid, then returns the [`Span`] to display the corresponding error at.
pub(crate) fn context_ty(sig: &syn::Signature) -> Result<Option<syn::Type>, Span> {
match sig.receiver() {
Some(syn::FnArg::Receiver(rcv)) => {
if rcv.reference.is_none() || rcv.mutability.is_some() {
return Err(rcv.span());
}
}
_ => return Err(sig.span()),
}
if sig.inputs.len() > 2 {
return Err(sig.inputs.span());
}
let second_arg_ty = match sig.inputs.iter().nth(1) {
Some(syn::FnArg::Typed(arg)) => &*arg.ty,
None => return Ok(None),
_ => return Err(sig.inputs.span()),
};
match second_arg_ty.unparenthesized() {
syn::Type::Reference(ref_ty) => {
if ref_ty.mutability.is_some() {
return Err(ref_ty.span());
}
Ok(Some(ref_ty.elem.unparenthesized().clone()))
}
ty => Err(ty.span()),
}
}

View file

@ -0,0 +1,261 @@
//! Common functions, definitions and extensions for parsing, normalizing and modifying Rust syntax,
//! used by this crate.
pub(crate) mod attr;
pub(crate) mod downcaster;
use std::{
any::TypeId,
iter::{self, FromIterator as _},
mem,
};
use proc_macro2::Span;
use syn::{
ext::IdentExt as _,
parse::{Parse, ParseBuffer},
parse_quote,
punctuated::Punctuated,
token::{self, Token},
};
/// Extension of [`ParseBuffer`] providing common function widely used by this crate for parsing.
pub(crate) trait ParseBufferExt {
/// Tries to parse `T` as the next token.
///
/// Doesn't move [`ParseStream`]'s cursor if there is no `T`.
fn try_parse<T: Default + Parse + Token>(&self) -> syn::Result<Option<T>>;
/// Checks whether next token is `T`.
///
/// Doesn't move [`ParseStream`]'s cursor.
#[must_use]
fn is_next<T: Default + Token>(&self) -> bool;
/// Parses next token as [`syn::Ident`] _allowing_ Rust keywords, while default [`Parse`]
/// implementation for [`syn::Ident`] disallows keywords.
///
/// Always moves [`ParseStream`]'s cursor.
fn parse_any_ident(&self) -> syn::Result<syn::Ident>;
/// Checks whether next token is a wrapper `W` and if yes, then parses the wrapped tokens as `T`
/// [`Punctuated`] with `P`. Otherwise, parses just `T`.
///
/// Always moves [`ParseStream`]'s cursor.
fn parse_maybe_wrapped_and_punctuated<T, W, P>(&self) -> syn::Result<Punctuated<T, P>>
where
T: Parse,
W: Default + Token + 'static,
P: Default + Parse + Token;
}
impl<'a> ParseBufferExt for ParseBuffer<'a> {
fn try_parse<T: Default + Parse + Token>(&self) -> syn::Result<Option<T>> {
Ok(if self.is_next::<T>() {
Some(self.parse()?)
} else {
None
})
}
fn is_next<T: Default + Token>(&self) -> bool {
self.lookahead1().peek(|_| T::default())
}
fn parse_any_ident(&self) -> syn::Result<syn::Ident> {
self.call(syn::Ident::parse_any)
}
fn parse_maybe_wrapped_and_punctuated<T, W, P>(&self) -> syn::Result<Punctuated<T, P>>
where
T: Parse,
W: Default + Token + 'static,
P: Default + Parse + Token,
{
Ok(if self.is_next::<W>() {
let inner;
if TypeId::of::<W>() == TypeId::of::<token::Bracket>() {
let _ = syn::bracketed!(inner in self);
} else if TypeId::of::<W>() == TypeId::of::<token::Brace>() {
let _ = syn::braced!(inner in self);
} else if TypeId::of::<W>() == TypeId::of::<token::Paren>() {
let _ = syn::parenthesized!(inner in self);
} else {
unimplemented!(
"ParseBufferExt::parse_maybe_wrapped_and_punctuated supports only brackets, \
braces and parentheses as wrappers.",
);
}
Punctuated::parse_terminated(&inner)?
} else {
Punctuated::from_iter(iter::once(self.parse::<T>()?))
})
}
}
/// Extension of [`syn::Type`] providing common function widely used by this crate for parsing.
pub(crate) trait TypeExt {
/// Retrieves the innermost non-parenthesized [`syn::Type`] from the given one (unwraps nested
/// [`syn::TypeParen`]s asap).
#[must_use]
fn unparenthesized(&self) -> &Self;
/// Retrieves the inner [`syn::Type`] from the given reference type, or just returns "as is" if
/// the type is not a reference.
///
/// Also, makes the type [`TypeExt::unparenthesized`], if possible.
#[must_use]
fn unreferenced(&self) -> &Self;
fn lifetimes_anonymized(&mut self);
}
impl TypeExt for syn::Type {
fn unparenthesized(&self) -> &Self {
match self {
Self::Paren(ty) => ty.elem.unparenthesized(),
ty => ty,
}
}
fn unreferenced(&self) -> &Self {
match self.unparenthesized() {
Self::Reference(ref_ty) => &*ref_ty.elem,
ty => ty,
}
}
fn lifetimes_anonymized(&mut self) {
use syn::{GenericArgument as GA, Type as T};
match self {
T::Array(syn::TypeArray { elem, .. })
| T::Group(syn::TypeGroup { elem, .. })
| T::Paren(syn::TypeParen { elem, .. })
| T::Ptr(syn::TypePtr { elem, .. })
| T::Slice(syn::TypeSlice { elem, .. }) => (&mut *elem).lifetimes_anonymized(),
T::Tuple(syn::TypeTuple { elems, .. }) => {
for ty in elems.iter_mut() {
ty.lifetimes_anonymized();
}
}
T::ImplTrait(syn::TypeImplTrait { bounds, .. })
| T::TraitObject(syn::TypeTraitObject { bounds, .. }) => {
for bound in bounds.iter_mut() {
match bound {
syn::TypeParamBound::Lifetime(lt) => {
lt.ident = syn::Ident::new("_", Span::call_site())
}
syn::TypeParamBound::Trait(_) => {
todo!("Anonymizing lifetimes in trait is not yet supported")
}
}
}
}
T::Reference(ref_ty) => {
if let Some(lt) = ref_ty.lifetime.as_mut() {
lt.ident = syn::Ident::new("_", Span::call_site());
}
(&mut *ref_ty.elem).lifetimes_anonymized();
}
T::Path(ty) => {
for seg in ty.path.segments.iter_mut() {
match &mut seg.arguments {
syn::PathArguments::AngleBracketed(angle) => {
for arg in angle.args.iter_mut() {
match arg {
GA::Lifetime(lt) => {
lt.ident = syn::Ident::new("_", Span::call_site());
}
GA::Type(ty) => ty.lifetimes_anonymized(),
GA::Binding(b) => b.ty.lifetimes_anonymized(),
GA::Constraint(_) | GA::Const(_) => {}
}
}
}
syn::PathArguments::Parenthesized(args) => {
for ty in args.inputs.iter_mut() {
ty.lifetimes_anonymized();
}
if let syn::ReturnType::Type(_, ty) = &mut args.output {
(&mut *ty).lifetimes_anonymized();
}
}
syn::PathArguments::None => {}
}
}
}
// These types unlikely will be used as GraphQL types.
T::BareFn(_)
| T::Infer(_)
| T::Macro(_)
| T::Never(_)
| T::Verbatim(_)
| T::__Nonexhaustive => {}
}
}
}
/// Extension of [`syn::Generics`] providing common function widely used by this crate for parsing.
pub(crate) trait GenericsExt {
/// Removes all default types out of type parameters and const parameters in these
/// [`syn::Generics`].
fn remove_defaults(&mut self);
/// Moves all trait and lifetime bounds of these [`syn::Generics`] to its [`syn::WhereClause`].
fn move_bounds_to_where_clause(&mut self);
}
impl GenericsExt for syn::Generics {
fn remove_defaults(&mut self) {
use syn::GenericParam as P;
for p in &mut self.params {
match p {
P::Type(p) => {
p.eq_token = None;
p.default = None;
}
P::Lifetime(_) => {}
P::Const(p) => {
p.eq_token = None;
p.default = None;
}
}
}
}
fn move_bounds_to_where_clause(&mut self) {
use syn::GenericParam as P;
let _ = self.make_where_clause();
let where_clause = self.where_clause.as_mut().unwrap();
for p in &mut self.params {
match p {
P::Type(p) => {
if p.colon_token.is_some() {
p.colon_token = None;
let bounds = mem::take(&mut p.bounds);
let ty = &p.ident;
where_clause.predicates.push(parse_quote! { #ty: #bounds });
}
}
P::Lifetime(p) => {
if p.colon_token.is_some() {
p.colon_token = None;
let bounds = mem::take(&mut p.bounds);
let lt = &p.lifetime;
where_clause.predicates.push(parse_quote! { #lt: #bounds });
}
}
P::Const(_) => {}
}
}
}
}

View file

@ -138,7 +138,7 @@ pub fn impl_enum(ast: syn::DeriveInput, error: GraphQLScope) -> syn::Result<Toke
fields,
// NOTICE: only unit variants allow -> no generics possible
generics: syn::Generics::default(),
interfaces: None,
interfaces: vec![],
include_type_generics: true,
generic_scalar: true,
no_async: attrs.no_async.is_some(),

View file

@ -138,7 +138,7 @@ pub fn impl_input_object(ast: syn::DeriveInput, error: GraphQLScope) -> syn::Res
description: attrs.description.map(SpanContainer::into_inner),
fields,
generics: ast.generics,
interfaces: None,
interfaces: vec![],
include_type_generics: true,
generic_scalar: true,
no_async: attrs.no_async.is_some(),

View file

@ -89,12 +89,6 @@ pub fn build_derive_object(ast: syn::DeriveInput, error: GraphQLScope) -> syn::R
// Early abort after checking all fields
proc_macro_error::abort_if_dirty();
if !attrs.interfaces.is_empty() {
attrs.interfaces.iter().for_each(|elm| {
error.unsupported_attribute(elm.span(), UnsupportedAttribute::Interface)
});
}
if let Some(duplicates) =
crate::util::duplicate::Duplicate::find_by_key(&fields, |field| field.name.as_str())
{
@ -124,7 +118,11 @@ pub fn build_derive_object(ast: syn::DeriveInput, error: GraphQLScope) -> syn::R
description: attrs.description.map(SpanContainer::into_inner),
fields,
generics: ast.generics,
interfaces: None,
interfaces: attrs
.interfaces
.into_iter()
.map(SpanContainer::into_inner)
.collect(),
include_type_generics: true,
generic_scalar: true,
no_async: attrs.no_async.is_some(),

View file

@ -1,10 +1,11 @@
use crate::{
common::parse::ParseBufferExt as _,
result::GraphQLScope,
util::{self, span_container::SpanContainer},
};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{self, spanned::Spanned, Data, Fields, Ident, Variant};
use syn::{spanned::Spanned, token, Data, Fields, Ident, Variant};
#[derive(Debug, Default)]
struct TransparentAttributes {
@ -25,12 +26,12 @@ impl syn::parse::Parse for TransparentAttributes {
let ident: syn::Ident = input.parse()?;
match ident.to_string().as_str() {
"name" => {
input.parse::<syn::Token![=]>()?;
input.parse::<token::Eq>()?;
let val = input.parse::<syn::LitStr>()?;
output.name = Some(val.value());
}
"description" => {
input.parse::<syn::Token![=]>()?;
input.parse::<token::Eq>()?;
let val = input.parse::<syn::LitStr>()?;
output.description = Some(val.value());
}
@ -39,9 +40,7 @@ impl syn::parse::Parse for TransparentAttributes {
}
_ => return Err(syn::Error::new(ident.span(), "unknown attribute")),
}
if input.lookahead1().peek(syn::Token![,]) {
input.parse::<syn::Token![,]>()?;
}
input.try_parse::<token::Comma>()?;
}
Ok(output)

View file

@ -0,0 +1,679 @@
//! Code generation for `#[graphql_interface]` macro.
use std::mem;
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens as _};
use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned};
use crate::{
common::{
parse::{self, TypeExt as _},
ScalarValueType,
},
result::GraphQLScope,
util::{path_eq_single, span_container::SpanContainer, to_camel_case},
};
use super::{
inject_async_trait, ArgumentMeta, Definition, EnumType, Field, FieldArgument, ImplMeta,
Implementer, ImplementerDowncast, MethodArgument, MethodMeta, TraitMeta, TraitObjectType, Type,
};
/// [`GraphQLScope`] of errors for `#[graphql_interface]` macro.
const ERR: GraphQLScope = GraphQLScope::InterfaceAttr;
/// Expands `#[graphql_interface]` macro into generated code.
pub fn expand(attr_args: TokenStream, body: TokenStream) -> syn::Result<TokenStream> {
if let Ok(mut ast) = syn::parse2::<syn::ItemTrait>(body.clone()) {
let trait_attrs = parse::attr::unite(("graphql_interface", &attr_args), &ast.attrs);
ast.attrs = parse::attr::strip("graphql_interface", ast.attrs);
return expand_on_trait(trait_attrs, ast);
} else if let Ok(mut ast) = syn::parse2::<syn::ItemImpl>(body) {
if ast.trait_.is_some() {
let impl_attrs = parse::attr::unite(("graphql_interface", &attr_args), &ast.attrs);
ast.attrs = parse::attr::strip("graphql_interface", ast.attrs);
return expand_on_impl(impl_attrs, ast);
}
}
Err(syn::Error::new(
Span::call_site(),
"#[graphql_interface] attribute is applicable to trait definitions and trait \
implementations only",
))
}
/// Expands `#[graphql_interface]` macro placed on trait definition.
pub fn expand_on_trait(
attrs: Vec<syn::Attribute>,
mut ast: syn::ItemTrait,
) -> syn::Result<TokenStream> {
let meta = TraitMeta::from_attrs("graphql_interface", &attrs)?;
let trait_ident = &ast.ident;
let trait_span = ast.span();
let name = meta
.name
.clone()
.map(SpanContainer::into_inner)
.unwrap_or_else(|| trait_ident.unraw().to_string());
if !meta.is_internal && name.starts_with("__") {
ERR.no_double_underscore(
meta.name
.as_ref()
.map(SpanContainer::span_ident)
.unwrap_or_else(|| trait_ident.span()),
);
}
let scalar = meta
.scalar
.as_ref()
.map(|sc| {
ast.generics
.params
.iter()
.find_map(|p| {
if let syn::GenericParam::Type(tp) = p {
let ident = &tp.ident;
let ty: syn::Type = parse_quote! { #ident };
if &ty == sc.as_ref() {
return Some(&tp.ident);
}
}
None
})
.map(|ident| ScalarValueType::ExplicitGeneric(ident.clone()))
.unwrap_or_else(|| ScalarValueType::Concrete(sc.as_ref().clone()))
})
.unwrap_or_else(|| ScalarValueType::ImplicitGeneric);
let mut implementers: Vec<_> = meta
.implementers
.iter()
.map(|ty| Implementer {
ty: ty.as_ref().clone(),
downcast: None,
context_ty: None,
scalar: scalar.clone(),
})
.collect();
for (ty, downcast) in &meta.external_downcasts {
match implementers.iter_mut().find(|i| &i.ty == ty) {
Some(impler) => {
impler.downcast = Some(ImplementerDowncast::External {
path: downcast.inner().clone(),
});
}
None => err_only_implementer_downcast(&downcast.span_joined()),
}
}
proc_macro_error::abort_if_dirty();
let mut fields = vec![];
for item in &mut ast.items {
if let syn::TraitItem::Method(m) = item {
match TraitMethod::parse(m) {
Some(TraitMethod::Field(f)) => fields.push(f),
Some(TraitMethod::Downcast(d)) => {
match implementers.iter_mut().find(|i| i.ty == d.ty) {
Some(impler) => {
if let Some(external) = &impler.downcast {
err_duplicate_downcast(m, external, &impler.ty);
} else {
impler.downcast = d.downcast;
impler.context_ty = d.context_ty;
}
}
None => err_only_implementer_downcast(&m.sig),
}
}
_ => {}
}
}
}
proc_macro_error::abort_if_dirty();
if fields.is_empty() {
ERR.emit_custom(trait_span, "must have at least one field");
}
if !all_fields_different(&fields) {
ERR.emit_custom(trait_span, "must have a different name for each field");
}
proc_macro_error::abort_if_dirty();
let context = meta
.context
.as_ref()
.map(|c| c.as_ref().clone())
.or_else(|| {
fields.iter().find_map(|f| {
f.arguments
.iter()
.find_map(MethodArgument::context_ty)
.cloned()
})
})
.or_else(|| {
implementers
.iter()
.find_map(|impler| impler.context_ty.as_ref())
.cloned()
});
let is_trait_object = meta.r#dyn.is_some();
let is_async_trait = meta.asyncness.is_some()
|| ast
.items
.iter()
.find_map(|item| match item {
syn::TraitItem::Method(m) => m.sig.asyncness,
_ => None,
})
.is_some();
let has_default_async_methods = ast.items.iter().any(|item| match item {
syn::TraitItem::Method(m) => m.sig.asyncness.and(m.default.as_ref()).is_some(),
_ => false,
});
let ty = if is_trait_object {
Type::TraitObject(Box::new(TraitObjectType::new(
&ast,
&meta,
scalar.clone(),
context.clone(),
)))
} else {
Type::Enum(Box::new(EnumType::new(
&ast,
&meta,
&implementers,
scalar.clone(),
)))
};
let generated_code = Definition {
ty,
name,
description: meta.description.map(SpanContainer::into_inner),
context,
scalar: scalar.clone(),
fields,
implementers,
};
// Attach the `juniper::AsDynGraphQLValue` on top of the trait if dynamic dispatch is used.
if is_trait_object {
ast.attrs.push(parse_quote! {
#[allow(unused_qualifications, clippy::type_repetition_in_bounds)]
});
let scalar_ty = scalar.generic_ty();
if !scalar.is_explicit_generic() {
let default_ty = scalar.default_ty();
ast.generics
.params
.push(parse_quote! { #scalar_ty = #default_ty });
}
ast.generics
.make_where_clause()
.predicates
.push(parse_quote! { #scalar_ty: ::juniper::ScalarValue });
ast.supertraits
.push(parse_quote! { ::juniper::AsDynGraphQLValue<#scalar_ty> });
}
if is_async_trait {
if has_default_async_methods {
// Hack for object safety. See details: https://docs.rs/async-trait/#dyn-traits
ast.supertraits.push(parse_quote! { Sync });
}
inject_async_trait(
&mut ast.attrs,
ast.items.iter_mut().filter_map(|i| {
if let syn::TraitItem::Method(m) = i {
Some(&mut m.sig)
} else {
None
}
}),
&ast.generics,
);
}
Ok(quote! {
#ast
#generated_code
})
}
/// Expands `#[graphql_interface]` macro placed on trait implementation block.
pub fn expand_on_impl(
attrs: Vec<syn::Attribute>,
mut ast: syn::ItemImpl,
) -> syn::Result<TokenStream> {
let meta = ImplMeta::from_attrs("graphql_interface", &attrs)?;
let is_async_trait = meta.asyncness.is_some()
|| ast
.items
.iter()
.find_map(|item| match item {
syn::ImplItem::Method(m) => m.sig.asyncness,
_ => None,
})
.is_some();
let is_trait_object = meta.r#dyn.is_some();
if is_trait_object {
let scalar = meta
.scalar
.as_ref()
.map(|sc| {
ast.generics
.params
.iter()
.find_map(|p| {
if let syn::GenericParam::Type(tp) = p {
let ident = &tp.ident;
let ty: syn::Type = parse_quote! { #ident };
if &ty == sc.as_ref() {
return Some(&tp.ident);
}
}
None
})
.map(|ident| ScalarValueType::ExplicitGeneric(ident.clone()))
.unwrap_or_else(|| ScalarValueType::Concrete(sc.as_ref().clone()))
})
.unwrap_or_else(|| ScalarValueType::ImplicitGeneric);
ast.attrs.push(parse_quote! {
#[allow(unused_qualifications, clippy::type_repetition_in_bounds)]
});
if scalar.is_implicit_generic() {
ast.generics.params.push(parse_quote! { #scalar });
}
if scalar.is_generic() {
ast.generics
.make_where_clause()
.predicates
.push(parse_quote! { #scalar: ::juniper::ScalarValue + Send + Sync });
}
if !scalar.is_explicit_generic() {
let (_, trait_path, _) = ast.trait_.as_mut().unwrap();
let trait_params = &mut trait_path.segments.last_mut().unwrap().arguments;
if let syn::PathArguments::None = trait_params {
*trait_params = syn::PathArguments::AngleBracketed(parse_quote! { <> });
}
if let syn::PathArguments::AngleBracketed(a) = trait_params {
a.args.push(parse_quote! { #scalar });
}
}
}
if is_async_trait {
inject_async_trait(
&mut ast.attrs,
ast.items.iter_mut().filter_map(|i| {
if let syn::ImplItem::Method(m) = i {
Some(&mut m.sig)
} else {
None
}
}),
&ast.generics,
);
}
Ok(quote! { #ast })
}
/// Representation of parsed Rust trait method for `#[graphql_interface]` macro code generation.
enum TraitMethod {
/// Method represents a [`Field`] of [GraphQL interface][1].
///
/// [1]: https://spec.graphql.org/June2018/#sec-Interfaces
Field(Field),
/// Method represents a custom downcasting function into the [`Implementer`] of
/// [GraphQL interface][1].
///
/// [1]: https://spec.graphql.org/June2018/#sec-Interfaces
Downcast(Box<Implementer>),
}
impl TraitMethod {
/// Parses this [`TraitMethod`] from the given trait method definition.
///
/// Returns [`None`] if the trait method marked with `#[graphql_interface(ignore)]` attribute,
/// or parsing fails.
#[must_use]
fn parse(method: &mut syn::TraitItemMethod) -> Option<Self> {
let method_attrs = method.attrs.clone();
// Remove repeated attributes from the method, to omit incorrect expansion.
method.attrs = mem::take(&mut method.attrs)
.into_iter()
.filter(|attr| !path_eq_single(&attr.path, "graphql_interface"))
.collect();
let meta = MethodMeta::from_attrs("graphql_interface", &method_attrs)
.map_err(|e| proc_macro_error::emit_error!(e))
.ok()?;
if meta.ignore.is_some() {
return None;
}
if meta.downcast.is_some() {
return Some(Self::Downcast(Box::new(Self::parse_downcast(method)?)));
}
Some(Self::Field(Self::parse_field(method, meta)?))
}
/// Parses [`TraitMethod::Downcast`] from the given trait method definition.
///
/// Returns [`None`] if parsing fails.
#[must_use]
fn parse_downcast(method: &mut syn::TraitItemMethod) -> Option<Implementer> {
let method_ident = &method.sig.ident;
let ty = parse::downcaster::output_type(&method.sig.output)
.map_err(|span| {
ERR.emit_custom(
span,
"expects trait method return type to be `Option<&ImplementerType>` only",
)
})
.ok()?;
let context_ty = parse::downcaster::context_ty(&method.sig)
.map_err(|span| {
ERR.emit_custom(
span,
"expects trait method to accept `&self` only and, optionally, `&Context`",
)
})
.ok()?;
if let Some(is_async) = &method.sig.asyncness {
ERR.emit_custom(
is_async.span(),
"async downcast to interface implementer is not supported",
);
return None;
}
let downcast = ImplementerDowncast::Method {
name: method_ident.clone(),
with_context: context_ty.is_some(),
};
Some(Implementer {
ty,
downcast: Some(downcast),
context_ty,
scalar: ScalarValueType::ImplicitGeneric,
})
}
/// Parses [`TraitMethod::Field`] from the given trait method definition.
///
/// Returns [`None`] if parsing fails.
#[must_use]
fn parse_field(method: &mut syn::TraitItemMethod, meta: MethodMeta) -> Option<Field> {
let method_ident = &method.sig.ident;
let name = meta
.name
.as_ref()
.map(|m| m.as_ref().value())
.unwrap_or_else(|| to_camel_case(&method_ident.unraw().to_string()));
if name.starts_with("__") {
ERR.no_double_underscore(
meta.name
.as_ref()
.map(SpanContainer::span_ident)
.unwrap_or_else(|| method_ident.span()),
);
return None;
}
let arguments = {
if method.sig.inputs.is_empty() {
return err_no_method_receiver(&method.sig.inputs);
}
let mut args_iter = method.sig.inputs.iter_mut();
match args_iter.next().unwrap() {
syn::FnArg::Receiver(rcv) => {
if rcv.reference.is_none() || rcv.mutability.is_some() {
return err_invalid_method_receiver(rcv);
}
}
syn::FnArg::Typed(arg) => {
if let syn::Pat::Ident(a) = &*arg.pat {
if a.ident.to_string().as_str() != "self" {
return err_invalid_method_receiver(arg);
}
}
return err_no_method_receiver(arg);
}
};
args_iter
.filter_map(|arg| match arg {
syn::FnArg::Receiver(_) => None,
syn::FnArg::Typed(arg) => Self::parse_field_argument(arg),
})
.collect()
};
let mut ty = match &method.sig.output {
syn::ReturnType::Default => parse_quote! { () },
syn::ReturnType::Type(_, ty) => ty.unparenthesized().clone(),
};
ty.lifetimes_anonymized();
let description = meta.description.as_ref().map(|d| d.as_ref().value());
let deprecated = meta
.deprecated
.as_ref()
.map(|d| d.as_ref().as_ref().map(syn::LitStr::value));
Some(Field {
name,
ty,
description,
deprecated,
method: method_ident.clone(),
arguments,
is_async: method.sig.asyncness.is_some(),
})
}
/// Parses [`MethodArgument`] from the given trait method argument definition.
///
/// Returns [`None`] if parsing fails.
#[must_use]
fn parse_field_argument(argument: &mut syn::PatType) -> Option<MethodArgument> {
let argument_attrs = argument.attrs.clone();
// Remove repeated attributes from the method, to omit incorrect expansion.
argument.attrs = mem::take(&mut argument.attrs)
.into_iter()
.filter(|attr| !path_eq_single(&attr.path, "graphql_interface"))
.collect();
let meta = ArgumentMeta::from_attrs("graphql_interface", &argument_attrs)
.map_err(|e| proc_macro_error::emit_error!(e))
.ok()?;
if meta.context.is_some() {
return Some(MethodArgument::Context(argument.ty.unreferenced().clone()));
}
if meta.executor.is_some() {
return Some(MethodArgument::Executor);
}
if let syn::Pat::Ident(name) = &*argument.pat {
let arg = match name.ident.unraw().to_string().as_str() {
"context" | "ctx" => {
Some(MethodArgument::Context(argument.ty.unreferenced().clone()))
}
"executor" => Some(MethodArgument::Executor),
_ => None,
};
if arg.is_some() {
ensure_no_regular_field_argument_meta(&meta)?;
return arg;
}
}
let name = if let Some(name) = meta.name.as_ref() {
name.as_ref().value()
} else if let syn::Pat::Ident(name) = &*argument.pat {
to_camel_case(&name.ident.unraw().to_string())
} else {
ERR.custom(
argument.pat.span(),
"trait method argument should be declared as a single identifier",
)
.note(String::from(
"use `#[graphql_interface(name = ...)]` attribute to specify custom argument's \
name without requiring it being a single identifier",
))
.emit();
return None;
};
if name.starts_with("__") {
ERR.no_double_underscore(
meta.name
.as_ref()
.map(SpanContainer::span_ident)
.unwrap_or_else(|| argument.pat.span()),
);
return None;
}
Some(MethodArgument::Regular(FieldArgument {
name,
ty: argument.ty.as_ref().clone(),
description: meta.description.as_ref().map(|d| d.as_ref().value()),
default: meta.default.as_ref().map(|v| v.as_ref().clone()),
}))
}
}
/// Checks whether the given [`ArgumentMeta`] doesn't contain arguments related to
/// [`FieldArgument`].
#[must_use]
fn ensure_no_regular_field_argument_meta(meta: &ArgumentMeta) -> Option<()> {
if let Some(span) = &meta.name {
return err_disallowed_attr(&span, "name");
}
if let Some(span) = &meta.description {
return err_disallowed_attr(&span, "description");
}
if let Some(span) = &meta.default {
return err_disallowed_attr(&span, "default");
}
Some(())
}
/// Emits "argument is not allowed" [`syn::Error`] for the given `arg` pointing to the given `span`.
#[must_use]
fn err_disallowed_attr<T, S: Spanned>(span: &S, arg: &str) -> Option<T> {
ERR.custom(
span.span(),
format!(
"attribute argument `#[graphql_interface({} = ...)]` is not allowed here",
arg,
),
)
.emit();
None
}
/// Emits "invalid trait method receiver" [`syn::Error`] pointing to the given `span`.
#[must_use]
fn err_invalid_method_receiver<T, S: Spanned>(span: &S) -> Option<T> {
ERR.custom(
span.span(),
"trait method receiver can only be a shared reference `&self`",
)
.emit();
None
}
/// Emits "no trait method receiver" [`syn::Error`] pointing to the given `span`.
#[must_use]
fn err_no_method_receiver<T, S: Spanned>(span: &S) -> Option<T> {
ERR.custom(
span.span(),
"trait method should have a shared reference receiver `&self`",
)
.emit();
None
}
/// Emits "non-implementer downcast target" [`syn::Error`] pointing to the given `span`.
fn err_only_implementer_downcast<S: Spanned>(span: &S) {
ERR.custom(
span.span(),
"downcasting is possible only to interface implementers",
)
.emit();
}
/// Emits "duplicate downcast" [`syn::Error`] for the given `method` and `external`
/// [`ImplementerDowncast`] function.
fn err_duplicate_downcast(
method: &syn::TraitItemMethod,
external: &ImplementerDowncast,
impler_ty: &syn::Type,
) {
let external = match external {
ImplementerDowncast::External { path } => path,
_ => unreachable!(),
};
ERR.custom(
method.span(),
format!(
"trait method `{}` conflicts with the external downcast function `{}` declared on the \
trait to downcast into the implementer type `{}`",
method.sig.ident,
external.to_token_stream(),
impler_ty.to_token_stream(),
),
)
.note(String::from(
"use `#[graphql_interface(ignore)]` attribute argument to ignore this trait method for \
interface implementers downcasting",
))
.emit()
}
/// Checks whether all [GraphQL interface][1] fields have different names.
///
/// [1]: https://spec.graphql.org/June2018/#sec-Interfaces
fn all_fields_different(fields: &[Field]) -> bool {
let mut names: Vec<_> = fields.iter().map(|f| &f.name).collect();
names.dedup();
names.len() == fields.len()
}

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,15 @@
//! Code generation for `#[graphql_union]` macro.
use std::{mem, ops::Deref as _};
use std::mem;
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens as _};
use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned as _};
use crate::{
common::parse,
result::GraphQLScope,
util::{path_eq_single, span_container::SpanContainer, unparenthesize},
util::{path_eq_single, span_container::SpanContainer},
};
use super::{
@ -27,17 +28,8 @@ pub fn expand(attr_args: TokenStream, body: TokenStream) -> syn::Result<TokenStr
"#[graphql_union] attribute is applicable to trait definitions only",
)
})?;
let mut trait_attrs = Vec::with_capacity(ast.attrs.len() + 1);
trait_attrs.push(parse_quote! { #[graphql_union(#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(|attr| !path_eq_single(&attr.path, "graphql_union"))
.collect();
let trait_attrs = parse::attr::unite(("graphql_union", &attr_args), &ast.attrs);
ast.attrs = parse::attr::strip("graphql_union", ast.attrs);
let meta = UnionMeta::from_attrs("graphql_union", &trait_attrs)?;
@ -98,7 +90,6 @@ pub fn expand(attr_args: TokenStream, body: TokenStream) -> syn::Result<TokenStr
scalar: meta.scalar.map(SpanContainer::into_inner),
generics: ast.generics.clone(),
variants,
span: trait_span,
};
Ok(quote! {
@ -149,7 +140,7 @@ fn parse_variant_from_trait_method(
let method_span = method.sig.span();
let method_ident = &method.sig.ident;
let ty = parse_trait_method_output_type(&method.sig)
let ty = parse::downcaster::output_type(&method.sig.output)
.map_err(|span| {
ERR.emit_custom(
span,
@ -157,7 +148,7 @@ fn parse_variant_from_trait_method(
)
})
.ok()?;
let method_context_ty = parse_trait_method_input_args(&method.sig)
let method_context_ty = parse::downcaster::context_ty(&method.sig)
.map_err(|span| {
ERR.emit_custom(
span,
@ -168,7 +159,7 @@ fn parse_variant_from_trait_method(
if let Some(is_async) = &method.sig.asyncness {
ERR.emit_custom(
is_async.span(),
"doesn't support async union variants resolvers yet",
"async downcast to union variants is not supported",
);
return None;
}
@ -220,85 +211,3 @@ fn parse_variant_from_trait_method(
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<syn::Type, Span> {
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<Option<syn::Type>, Span> {
match sig.receiver() {
Some(syn::FnArg::Receiver(rcv)) => {
if rcv.reference.is_none() || rcv.mutability.is_some() {
return Err(rcv.span());
}
}
_ => return Err(sig.span()),
}
if sig.inputs.len() > 2 {
return Err(sig.inputs.span());
}
let second_arg_ty = match sig.inputs.iter().nth(1) {
Some(syn::FnArg::Typed(arg)) => arg.ty.deref(),
None => return Ok(None),
_ => return Err(sig.inputs.span()),
};
match unparenthesize(second_arg_ty) {
syn::Type::Reference(ref_ty) => {
if ref_ty.mutability.is_some() {
return Err(ref_ty.span());
}
Ok(Some(ref_ty.elem.deref().clone()))
}
ty => Err(ty.span()),
}
}

View file

@ -6,8 +6,7 @@ use quote::{quote, ToTokens};
use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned as _, Data, Fields};
use crate::{
result::GraphQLScope,
util::{span_container::SpanContainer, unparenthesize},
common::parse::TypeExt as _, result::GraphQLScope, util::span_container::SpanContainer,
};
use super::{
@ -85,7 +84,6 @@ fn expand_enum(ast: syn::DeriveInput) -> syn::Result<UnionDefinition> {
scalar: meta.scalar.map(SpanContainer::into_inner),
generics: ast.generics,
variants,
span: enum_span,
})
}
@ -115,7 +113,7 @@ fn parse_variant_from_enum_variant(
let mut iter = fields.unnamed.iter();
let first = iter.next().unwrap();
if iter.next().is_none() {
Ok(unparenthesize(&first.ty).clone())
Ok(first.ty.unparenthesized().clone())
} else {
Err(fields.span())
}
@ -214,6 +212,5 @@ fn expand_struct(ast: syn::DeriveInput) -> syn::Result<UnionDefinition> {
scalar: meta.scalar.map(SpanContainer::into_inner),
generics: ast.generics,
variants,
span: struct_span,
})
}

View file

@ -13,60 +13,16 @@ use syn::{
parse::{Parse, ParseStream},
parse_quote,
spanned::Spanned as _,
token,
};
use crate::util::{filter_attrs, get_doc_comment, span_container::SpanContainer, OptionExt as _};
/// Attempts to merge an [`Option`]ed `$field` of a `$self` struct with the same `$field` of
/// `$another` struct. If both are [`Some`], then throws a duplication error with a [`Span`] related
/// to the `$another` struct (a later one).
///
/// The type of [`Span`] may be explicitly specified as one of the [`SpanContainer`] methods.
/// By default, [`SpanContainer::span_ident`] is used.
macro_rules! try_merge_opt {
($field:ident: $self:ident, $another:ident => $span:ident) => {{
if let Some(v) = $self.$field {
$another
.$field
.replace(v)
.none_or_else(|dup| dup_attr_err(dup.$span()))?;
}
$another.$field
}};
($field:ident: $self:ident, $another:ident) => {
try_merge_opt!($field: $self, $another => span_ident)
};
}
/// Attempts to merge a [`HashMap`]ed `$field` of a `$self` struct with the same `$field` of
/// `$another` struct. If some [`HashMap`] entries are duplicated, then throws a duplication error
/// with a [`Span`] related to the `$another` struct (a later one).
///
/// The type of [`Span`] may be explicitly specified as one of the [`SpanContainer`] methods.
/// By default, [`SpanContainer::span_ident`] is used.
macro_rules! try_merge_hashmap {
($field:ident: $self:ident, $another:ident => $span:ident) => {{
if !$self.$field.is_empty() {
for (ty, rslvr) in $self.$field {
$another
.$field
.insert(ty, rslvr)
.none_or_else(|dup| dup_attr_err(dup.$span()))?;
}
}
$another.$field
}};
($field:ident: $self:ident, $another:ident) => {
try_merge_hashmap!($field: $self, $another => span_ident)
};
}
/// Creates and returns duplication error pointing to the given `span`.
fn dup_attr_err(span: Span) -> syn::Error {
syn::Error::new(span, "duplicated attribute")
}
use crate::{
common::parse::{
attr::{err, OptionExt as _},
ParseBufferExt as _,
},
util::{filter_attrs, get_doc_comment, span_container::SpanContainer},
};
/// Helper alias for the type of [`UnionMeta::external_resolvers`] field.
type UnionMetaResolvers = HashMap<syn::Type, SpanContainer<syn::ExprPath>>;
@ -131,10 +87,10 @@ impl Parse for UnionMeta {
let mut output = Self::default();
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
let ident = input.parse::<syn::Ident>()?;
match ident.to_string().as_str() {
"name" => {
input.parse::<syn::Token![=]>()?;
input.parse::<token::Eq>()?;
let name = input.parse::<syn::LitStr>()?;
output
.name
@ -143,10 +99,10 @@ impl Parse for UnionMeta {
Some(name.span()),
name.value(),
))
.none_or_else(|_| dup_attr_err(ident.span()))?
.none_or_else(|_| err::dup_arg(&ident))?
}
"desc" | "description" => {
input.parse::<syn::Token![=]>()?;
input.parse::<token::Eq>()?;
let desc = input.parse::<syn::LitStr>()?;
output
.description
@ -155,45 +111,43 @@ impl Parse for UnionMeta {
Some(desc.span()),
desc.value(),
))
.none_or_else(|_| dup_attr_err(ident.span()))?
.none_or_else(|_| err::dup_arg(&ident))?
}
"ctx" | "context" | "Context" => {
input.parse::<syn::Token![=]>()?;
input.parse::<token::Eq>()?;
let ctx = input.parse::<syn::Type>()?;
output
.context
.replace(SpanContainer::new(ident.span(), Some(ctx.span()), ctx))
.none_or_else(|_| dup_attr_err(ident.span()))?
.none_or_else(|_| err::dup_arg(&ident))?
}
"scalar" | "Scalar" | "ScalarValue" => {
input.parse::<syn::Token![=]>()?;
input.parse::<token::Eq>()?;
let scl = input.parse::<syn::Type>()?;
output
.scalar
.replace(SpanContainer::new(ident.span(), Some(scl.span()), scl))
.none_or_else(|_| dup_attr_err(ident.span()))?
.none_or_else(|_| err::dup_arg(&ident))?
}
"on" => {
let ty = input.parse::<syn::Type>()?;
input.parse::<syn::Token![=]>()?;
input.parse::<token::Eq>()?;
let rslvr = input.parse::<syn::ExprPath>()?;
let rslvr_spanned = SpanContainer::new(ident.span(), Some(ty.span()), rslvr);
let rslvr_span = rslvr_spanned.span_joined();
output
.external_resolvers
.insert(ty, rslvr_spanned)
.none_or_else(|_| dup_attr_err(rslvr_span))?
.none_or_else(|_| err::dup_arg(rslvr_span))?
}
"internal" => {
output.is_internal = true;
}
_ => {
return Err(syn::Error::new(ident.span(), "unknown attribute"));
name => {
return Err(err::unknown_arg(&ident, name));
}
}
if input.lookahead1().peek(syn::Token![,]) {
input.parse::<syn::Token![,]>()?;
}
input.try_parse::<token::Comma>()?;
}
Ok(output)
@ -201,19 +155,22 @@ impl Parse for UnionMeta {
}
impl UnionMeta {
/// Tries to merge two [`UnionMeta`]s into single one, reporting about duplicates, if any.
/// Tries to merge two [`UnionMeta`]s into a single one, reporting about duplicates, if any.
fn try_merge(self, mut another: Self) -> syn::Result<Self> {
Ok(Self {
name: try_merge_opt!(name: self, another),
description: try_merge_opt!(description: self, another),
context: try_merge_opt!(context: self, another),
scalar: try_merge_opt!(scalar: self, another),
external_resolvers: try_merge_hashmap!(external_resolvers: self, another => span_joined),
external_resolvers: try_merge_hashmap!(
external_resolvers: self, another => span_joined
),
is_internal: self.is_internal || another.is_internal,
})
}
/// Parses [`UnionMeta`] from the given multiple `name`d attributes placed on type definition.
/// Parses [`UnionMeta`] from the given multiple `name`d [`syn::Attribute`]s placed on a type
/// definition.
pub fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result<Self> {
let mut meta = filter_attrs(name, attrs)
.map(|attr| attr.parse_args())
@ -254,27 +211,25 @@ impl Parse for UnionVariantMeta {
let mut output = Self::default();
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
let ident = input.parse::<syn::Ident>()?;
match ident.to_string().as_str() {
"ignore" | "skip" => output
.ignore
.replace(SpanContainer::new(ident.span(), None, ident.clone()))
.none_or_else(|_| dup_attr_err(ident.span()))?,
.none_or_else(|_| err::dup_arg(&ident))?,
"with" => {
input.parse::<syn::Token![=]>()?;
input.parse::<token::Eq>()?;
let rslvr = input.parse::<syn::ExprPath>()?;
output
.external_resolver
.replace(SpanContainer::new(ident.span(), Some(rslvr.span()), rslvr))
.none_or_else(|_| dup_attr_err(ident.span()))?
.none_or_else(|_| err::dup_arg(&ident))?
}
_ => {
return Err(syn::Error::new(ident.span(), "unknown attribute"));
name => {
return Err(err::unknown_arg(&ident, name));
}
}
if input.lookahead1().peek(syn::Token![,]) {
input.parse::<syn::Token![,]>()?;
}
input.try_parse::<token::Comma>()?;
}
Ok(output)
@ -282,7 +237,7 @@ impl Parse for UnionVariantMeta {
}
impl UnionVariantMeta {
/// Tries to merge two [`UnionVariantMeta`]s into single one, reporting about duplicates, if
/// Tries to merge two [`UnionVariantMeta`]s into a single one, reporting about duplicates, if
/// any.
fn try_merge(self, mut another: Self) -> syn::Result<Self> {
Ok(Self {
@ -291,7 +246,7 @@ impl UnionVariantMeta {
})
}
/// Parses [`UnionVariantMeta`] from the given multiple `name`d attributes placed on
/// Parses [`UnionVariantMeta`] from the given multiple `name`d [`syn::Attribute`]s placed on a
/// variant/field/method definition.
pub fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result<Self> {
filter_attrs(name, attrs)
@ -356,6 +311,8 @@ struct UnionDefinition {
pub ty: syn::Type,
/// Generics of the Rust type that this [GraphQL union][1] is implemented for.
///
/// [1]: https://spec.graphql.org/June2018/#sec-Unions
pub generics: syn::Generics,
/// Indicator whether code should be generated for a trait object, rather than for a regular
@ -390,11 +347,6 @@ struct UnionDefinition {
///
/// [1]: https://spec.graphql.org/June2018/#sec-Unions
pub variants: Vec<UnionVariantDefinition>,
/// [`Span`] that points to the Rust source code which defines this [GraphQL union][1].
///
/// [1]: https://spec.graphql.org/June2018/#sec-Unions
pub span: Span,
}
impl ToTokens for UnionDefinition {
@ -432,7 +384,7 @@ impl ToTokens for UnionDefinition {
let var_check = &var.resolver_check;
quote! {
if #var_check {
return <#var_ty as ::juniper::GraphQLType<#scalar>>::name(&())
return <#var_ty as ::juniper::GraphQLType<#scalar>>::name(info)
.unwrap().to_string();
}
}
@ -442,15 +394,15 @@ impl ToTokens for UnionDefinition {
let resolve_into_type = self.variants.iter().zip(match_resolves.iter()).map(|(var, expr)| {
let var_ty = &var.ty;
let get_name = quote! { (<#var_ty as ::juniper::GraphQLType<#scalar>>::name(&())) };
let get_name = quote! { (<#var_ty as ::juniper::GraphQLType<#scalar>>::name(info)) };
quote! {
if type_name == #get_name.unwrap() {
return ::juniper::IntoResolvable::into(
{ #expr },
executor.context()
executor.context(),
)
.and_then(|res| match res {
Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(&(), &r),
Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(info, &r),
None => Ok(::juniper::Value::null()),
});
}
@ -464,19 +416,19 @@ impl ToTokens for UnionDefinition {
let var_ty = &var.ty;
let get_name = quote! {
(<#var_ty as ::juniper::GraphQLType<#scalar>>::name(&()))
(<#var_ty as ::juniper::GraphQLType<#scalar>>::name(info))
};
quote! {
if type_name == #get_name.unwrap() {
let res = ::juniper::IntoResolvable::into(
{ #expr },
executor.context()
executor.context(),
);
return Box::pin(async move {
match res? {
Some((ctx, r)) => {
let subexec = executor.replaced_context(ctx);
subexec.resolve_with_ctx_async(&(), &r).await
subexec.resolve_with_ctx_async(info, &r).await
},
None => Ok(::juniper::Value::null()),
}
@ -494,8 +446,7 @@ impl ToTokens for UnionDefinition {
if self.scalar.is_none() {
ext_generics.params.push(parse_quote! { #scalar });
ext_generics
.where_clause
.get_or_insert_with(|| parse_quote! { where })
.make_where_clause()
.predicates
.push(parse_quote! { #scalar: ::juniper::ScalarValue });
}
@ -531,10 +482,10 @@ impl ToTokens for UnionDefinition {
) -> ::juniper::meta::MetaType<'r, #scalar>
where #scalar: 'r,
{
let types = &[
#( registry.get_type::<&#var_types>(&(())), )*
let types = [
#( registry.get_type::<#var_types>(info), )*
];
registry.build_union_type::<#ty_full>(info, types)
registry.build_union_type::<#ty_full>(info, &types)
#description
.into_meta()
}
@ -556,7 +507,7 @@ impl ToTokens for UnionDefinition {
fn concrete_type_name(
&self,
context: &Self::Context,
_: &Self::TypeInfo,
info: &Self::TypeInfo,
) -> String {
#( #match_names )*
panic!(
@ -568,7 +519,7 @@ impl ToTokens for UnionDefinition {
fn resolve_into_type(
&self,
_: &Self::TypeInfo,
info: &Self::TypeInfo,
type_name: &str,
_: Option<&[::juniper::Selection<#scalar>]>,
executor: &::juniper::Executor<Self::Context, #scalar>,
@ -590,7 +541,7 @@ impl ToTokens for UnionDefinition {
{
fn resolve_into_type_async<'b>(
&'b self,
_: &'b Self::TypeInfo,
info: &'b Self::TypeInfo,
type_name: &str,
_: Option<&'b [::juniper::Selection<'b, #scalar>]>,
executor: &'b ::juniper::Executor<'b, 'b, Self::Context, #scalar>

View file

@ -201,18 +201,12 @@ fn create(
description: _impl.description,
fields,
generics: _impl.generics.clone(),
interfaces: if !_impl.attrs.interfaces.is_empty() {
Some(
_impl
.attrs
.interfaces
.into_iter()
.map(SpanContainer::into_inner)
.collect(),
)
} else {
None
},
interfaces: _impl
.attrs
.interfaces
.into_iter()
.map(SpanContainer::into_inner)
.collect(),
include_type_generics: false,
generic_scalar: false,
no_async: _impl.attrs.no_async.is_some(),

View file

@ -26,12 +26,10 @@ fn get_first_method_arg(
inputs: syn::punctuated::Punctuated<syn::FnArg, syn::Token![,]>,
) -> Option<syn::Ident> {
if let Some(fn_arg) = inputs.first() {
match fn_arg {
syn::FnArg::Typed(pat_type) => match &*pat_type.pat {
syn::Pat::Ident(pat_ident) => return Some(pat_ident.ident.clone()),
_ => (),
},
_ => (),
if let syn::FnArg::Typed(pat_type) = fn_arg {
if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
return Some(pat_ident.ident.clone());
}
}
}
@ -70,19 +68,11 @@ fn get_enum_type(return_type: &Option<syn::Type>) -> Option<syn::PathSegment> {
}
});
if let Some(generic_type_arg) = generic_type_arg {
match generic_type_arg {
syn::GenericArgument::Type(the_type) => match the_type {
syn::Type::Path(type_path) => {
if let Some(path_segment) =
type_path.path.segments.first()
{
return Some(path_segment.clone());
}
}
_ => (),
},
_ => (),
if let Some(syn::GenericArgument::Type(syn::Type::Path(type_path))) =
generic_type_arg
{
if let Some(path_segment) = type_path.path.segments.first() {
return Some(path_segment.clone());
}
}
}
@ -115,13 +105,10 @@ impl syn::parse::Parse for ScalarCodegenInput {
let custom_data_type_is_struct: bool =
!parse_custom_scalar_value_impl.generics.params.is_empty();
match *parse_custom_scalar_value_impl.self_ty {
syn::Type::Path(type_path) => {
if let Some(path_segment) = type_path.path.segments.first() {
impl_for_type = Some(path_segment.clone());
}
if let syn::Type::Path(type_path) = *parse_custom_scalar_value_impl.self_ty {
if let Some(path_segment) = type_path.path.segments.first() {
impl_for_type = Some(path_segment.clone());
}
_ => (),
}
for impl_item in parse_custom_scalar_value_impl.items {

View file

@ -12,6 +12,102 @@ extern crate proc_macro;
mod result;
mod util;
// NOTICE: Unfortunately this macro MUST be defined here, in the crate's root module, because Rust
// doesn't allow to export `macro_rules!` macros from a `proc-macro` crate type currently,
// and so we cannot move the definition into a sub-module and use the `#[macro_export]`
// attribute.
/// Attempts to merge an [`Option`]ed `$field` of a `$self` struct with the same `$field` of
/// `$another` struct. If both are [`Some`], then throws a duplication error with a [`Span`] related
/// to the `$another` struct (a later one).
///
/// The type of [`Span`] may be explicitly specified as one of the [`SpanContainer`] methods.
/// By default, [`SpanContainer::span_ident`] is used.
///
/// [`Span`]: proc_macro2::Span
/// [`SpanContainer`]: crate::util::span_container::SpanContainer
/// [`SpanContainer::span_ident`]: crate::util::span_container::SpanContainer::span_ident
macro_rules! try_merge_opt {
($field:ident: $self:ident, $another:ident => $span:ident) => {{
if let Some(v) = $self.$field {
$another
.$field
.replace(v)
.none_or_else(|dup| crate::common::parse::attr::err::dup_arg(&dup.$span()))?;
}
$another.$field
}};
($field:ident: $self:ident, $another:ident) => {
try_merge_opt!($field: $self, $another => span_ident)
};
}
// NOTICE: Unfortunately this macro MUST be defined here, in the crate's root module, because Rust
// doesn't allow to export `macro_rules!` macros from a `proc-macro` crate type currently,
// and so we cannot move the definition into a sub-module and use the `#[macro_export]`
// attribute.
/// Attempts to merge a [`HashMap`] `$field` of a `$self` struct with the same `$field` of
/// `$another` struct. If some [`HashMap`] entries are duplicated, then throws a duplication error
/// with a [`Span`] related to the `$another` struct (a later one).
///
/// The type of [`Span`] may be explicitly specified as one of the [`SpanContainer`] methods.
/// By default, [`SpanContainer::span_ident`] is used.
///
/// [`HashMap`]: std::collections::HashMap
/// [`Span`]: proc_macro2::Span
/// [`SpanContainer`]: crate::util::span_container::SpanContainer
/// [`SpanContainer::span_ident`]: crate::util::span_container::SpanContainer::span_ident
macro_rules! try_merge_hashmap {
($field:ident: $self:ident, $another:ident => $span:ident) => {{
if !$self.$field.is_empty() {
for (ty, rslvr) in $self.$field {
$another
.$field
.insert(ty, rslvr)
.none_or_else(|dup| crate::common::parse::attr::err::dup_arg(&dup.$span()))?;
}
}
$another.$field
}};
($field:ident: $self:ident, $another:ident) => {
try_merge_hashmap!($field: $self, $another => span_ident)
};
}
// NOTICE: Unfortunately this macro MUST be defined here, in the crate's root module, because Rust
// doesn't allow to export `macro_rules!` macros from a `proc-macro` crate type currently,
// and so we cannot move the definition into a sub-module and use the `#[macro_export]`
// attribute.
/// Attempts to merge a [`HashSet`] `$field` of a `$self` struct with the same `$field` of
/// `$another` struct. If some [`HashSet`] entries are duplicated, then throws a duplication error
/// with a [`Span`] related to the `$another` struct (a later one).
///
/// The type of [`Span`] may be explicitly specified as one of the [`SpanContainer`] methods.
/// By default, [`SpanContainer::span_ident`] is used.
///
/// [`HashSet`]: std::collections::HashSet
/// [`Span`]: proc_macro2::Span
/// [`SpanContainer`]: crate::util::span_container::SpanContainer
/// [`SpanContainer::span_ident`]: crate::util::span_container::SpanContainer::span_ident
macro_rules! try_merge_hashset {
($field:ident: $self:ident, $another:ident => $span:ident) => {{
if !$self.$field.is_empty() {
for ty in $self.$field {
$another
.$field
.replace(ty)
.none_or_else(|dup| crate::common::parse::attr::err::dup_arg(&dup.$span()))?;
}
}
$another.$field
}};
($field:ident: $self:ident, $another:ident) => {
try_merge_hashset!($field: $self, $another => span_ident)
};
}
mod derive_enum;
mod derive_input_object;
mod derive_object;
@ -19,6 +115,8 @@ mod derive_scalar_value;
mod impl_object;
mod impl_scalar;
mod common;
mod graphql_interface;
mod graphql_union;
use proc_macro::TokenStream;
@ -452,6 +550,14 @@ pub fn graphql_subscription(args: TokenStream, input: TokenStream) -> TokenStrea
))
}
#[proc_macro_error]
#[proc_macro_attribute]
pub fn graphql_interface(attr: TokenStream, body: TokenStream) -> TokenStream {
self::graphql_interface::attr::expand(attr.into(), body.into())
.unwrap_or_abort()
.into()
}
/// `#[derive(GraphQLUnion)]` macro for deriving a [GraphQL union][1] implementation for enums and
/// structs.
///

View file

@ -10,10 +10,11 @@ pub const SPEC_URL: &str = "https://spec.graphql.org/June2018/";
#[allow(unused_variables)]
pub enum GraphQLScope {
InterfaceAttr,
UnionAttr,
UnionDerive,
DeriveObject,
DeriveInputObject,
UnionDerive,
DeriveEnum,
DeriveScalar,
ImplScalar,
@ -23,9 +24,10 @@ pub enum GraphQLScope {
impl GraphQLScope {
pub fn spec_section(&self) -> &str {
match self {
Self::InterfaceAttr => "#sec-Interfaces",
Self::UnionAttr | Self::UnionDerive => "#sec-Unions",
Self::DeriveObject | Self::ImplObject => "#sec-Objects",
Self::DeriveInputObject => "#sec-Input-Objects",
Self::UnionAttr | Self::UnionDerive => "#sec-Unions",
Self::DeriveEnum => "#sec-Enums",
Self::DeriveScalar | Self::ImplScalar => "#sec-Scalars",
}
@ -35,9 +37,10 @@ impl GraphQLScope {
impl fmt::Display for GraphQLScope {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let name = match self {
Self::InterfaceAttr => "interface",
Self::UnionAttr | Self::UnionDerive => "union",
Self::DeriveObject | Self::ImplObject => "object",
Self::DeriveInputObject => "input object",
Self::UnionAttr | Self::UnionDerive => "union",
Self::DeriveEnum => "enum",
Self::DeriveScalar | Self::ImplScalar => "scalar",
};

View file

@ -1,23 +1,24 @@
#![allow(clippy::single_match)]
pub mod duplicate;
pub mod option_ext;
pub mod parse_impl;
pub mod span_container;
use std::ops::Deref as _;
use std::collections::HashMap;
use proc_macro2::{Span, TokenStream};
use proc_macro_error::abort;
use quote::quote;
use span_container::SpanContainer;
use std::collections::HashMap;
use syn::{
parse, parse_quote, punctuated::Punctuated, spanned::Spanned, Attribute, Lit, Meta, MetaList,
MetaNameValue, NestedMeta, Token,
parse::{Parse, ParseStream},
parse_quote,
punctuated::Punctuated,
spanned::Spanned,
token, Attribute, Lit, Meta, MetaList, MetaNameValue, NestedMeta,
};
pub use self::option_ext::OptionExt;
use crate::common::parse::ParseBufferExt as _;
/// Returns the name of a type.
/// If the type does not end in a simple ident, `None` is returned.
@ -74,15 +75,6 @@ pub fn type_is_identifier_ref(ty: &syn::Type, name: &str) -> bool {
}
}
/// Retrieves the innermost non-parenthesized [`syn::Type`] from the given one (unwraps nested
/// [`syn::TypeParen`]s asap).
pub fn unparenthesize(ty: &syn::Type) -> &syn::Type {
match ty {
syn::Type::Paren(ty) => unparenthesize(ty.elem.deref()),
_ => ty,
}
}
#[derive(Debug)]
pub struct DeprecationAttr {
pub reason: Option<String>,
@ -232,11 +224,18 @@ fn get_doc_attr(attrs: &[Attribute]) -> Option<Vec<MetaNameValue>> {
pub fn to_camel_case(s: &str) -> String {
let mut dest = String::new();
// handle '_' to be more friendly with the
// _var convention for unused variables
let s_iter = if s.starts_with('_') { &s[1..] } else { s }
.split('_')
.enumerate();
// Handle `_` and `__` to be more friendly with the `_var` convention for unused variables, and
// GraphQL introspection identifiers.
let s_iter = if s.starts_with("__") {
dest.push_str("__");
&s[2..]
} else if s.starts_with('_') {
&s[1..]
} else {
s
}
.split('_')
.enumerate();
for (i, part) in s_iter {
if i > 0 && part.len() == 1 {
@ -307,15 +306,15 @@ pub struct ObjectAttributes {
pub is_internal: bool,
}
impl syn::parse::Parse for ObjectAttributes {
fn parse(input: syn::parse::ParseStream) -> syn::parse::Result<Self> {
impl Parse for ObjectAttributes {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut output = Self::default();
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
let ident = input.parse_any_ident()?;
match ident.to_string().as_str() {
"name" => {
input.parse::<syn::Token![=]>()?;
input.parse::<token::Eq>()?;
let val = input.parse::<syn::LitStr>()?;
output.name = Some(SpanContainer::new(
ident.span(),
@ -324,7 +323,7 @@ impl syn::parse::Parse for ObjectAttributes {
));
}
"description" => {
input.parse::<syn::Token![=]>()?;
input.parse::<token::Eq>()?;
let val = input.parse::<syn::LitStr>()?;
output.description = Some(SpanContainer::new(
ident.span(),
@ -333,7 +332,7 @@ impl syn::parse::Parse for ObjectAttributes {
));
}
"context" | "Context" => {
input.parse::<syn::Token![=]>()?;
input.parse::<token::Eq>()?;
// TODO: remove legacy support for string based Context.
let ctx = if let Ok(val) = input.parse::<syn::LitStr>() {
eprintln!("DEPRECATION WARNING: using a string literal for the Context is deprecated");
@ -345,19 +344,15 @@ impl syn::parse::Parse for ObjectAttributes {
output.context = Some(SpanContainer::new(ident.span(), Some(ctx.span()), ctx));
}
"scalar" | "Scalar" => {
input.parse::<syn::Token![=]>()?;
input.parse::<token::Eq>()?;
let val = input.parse::<syn::Type>()?;
output.scalar = Some(SpanContainer::new(ident.span(), Some(val.span()), val));
}
"interfaces" => {
input.parse::<syn::Token![=]>()?;
let content;
syn::bracketed!(content in input);
output.interfaces =
syn::punctuated::Punctuated::<syn::Type, syn::Token![,]>::parse_terminated(
&content,
)?
.into_iter()
"impl" | "implements" | "interfaces" => {
input.parse::<token::Eq>()?;
output.interfaces = input.parse_maybe_wrapped_and_punctuated::<
syn::Type, token::Bracket, token::Comma,
>()?.into_iter()
.map(|interface| {
SpanContainer::new(ident.span(), Some(interface.span()), interface)
})
@ -374,9 +369,7 @@ impl syn::parse::Parse for ObjectAttributes {
return Err(syn::Error::new(ident.span(), "unknown attribute"));
}
}
if input.lookahead1().peek(syn::Token![,]) {
input.parse::<syn::Token![,]>()?;
}
input.try_parse::<token::Comma>()?;
}
Ok(output)
@ -384,7 +377,7 @@ impl syn::parse::Parse for ObjectAttributes {
}
impl ObjectAttributes {
pub fn from_attrs(attrs: &[syn::Attribute]) -> syn::parse::Result<Self> {
pub fn from_attrs(attrs: &[syn::Attribute]) -> syn::Result<Self> {
let attr_opt = find_graphql_attr(attrs);
if let Some(attr) = attr_opt {
// Need to unwrap outer (), which are not present for proc macro attributes,
@ -411,8 +404,8 @@ pub struct FieldAttributeArgument {
pub description: Option<syn::LitStr>,
}
impl parse::Parse for FieldAttributeArgument {
fn parse(input: parse::ParseStream) -> parse::Result<Self> {
impl Parse for FieldAttributeArgument {
fn parse(input: ParseStream) -> syn::Result<Self> {
let name = input.parse()?;
let mut arg = Self {
@ -426,7 +419,7 @@ impl parse::Parse for FieldAttributeArgument {
syn::parenthesized!(content in input);
while !content.is_empty() {
let name = content.parse::<syn::Ident>()?;
content.parse::<Token![=]>()?;
content.parse::<token::Eq>()?;
match name.to_string().as_str() {
"name" => {
@ -443,7 +436,7 @@ impl parse::Parse for FieldAttributeArgument {
}
// Discard trailing comma.
content.parse::<Token![,]>().ok();
content.parse::<token::Comma>().ok();
}
Ok(arg)
@ -465,13 +458,13 @@ enum FieldAttribute {
Default(SpanContainer<Option<syn::Expr>>),
}
impl parse::Parse for FieldAttribute {
fn parse(input: parse::ParseStream) -> parse::Result<Self> {
impl Parse for FieldAttribute {
fn parse(input: ParseStream) -> syn::Result<Self> {
let ident = input.parse::<syn::Ident>()?;
match ident.to_string().as_str() {
"name" => {
input.parse::<Token![=]>()?;
input.parse::<token::Eq>()?;
let lit = input.parse::<syn::LitStr>()?;
let raw = lit.value();
if !is_valid_name(&raw) {
@ -485,7 +478,7 @@ impl parse::Parse for FieldAttribute {
}
}
"description" => {
input.parse::<Token![=]>()?;
input.parse::<token::Eq>()?;
let lit = input.parse::<syn::LitStr>()?;
Ok(FieldAttribute::Description(SpanContainer::new(
ident.span(),
@ -494,8 +487,8 @@ impl parse::Parse for FieldAttribute {
)))
}
"deprecated" | "deprecation" => {
let reason = if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
let reason = if input.peek(token::Eq) {
input.parse::<token::Eq>()?;
Some(input.parse::<syn::LitStr>()?)
} else {
None
@ -516,7 +509,7 @@ impl parse::Parse for FieldAttribute {
"arguments" => {
let arg_content;
syn::parenthesized!(arg_content in input);
let args = Punctuated::<FieldAttributeArgument, Token![,]>::parse_terminated(
let args = Punctuated::<FieldAttributeArgument, token::Comma>::parse_terminated(
&arg_content,
)?;
let map = args
@ -526,8 +519,8 @@ impl parse::Parse for FieldAttribute {
Ok(FieldAttribute::Arguments(map))
}
"default" => {
let default_expr = if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
let default_expr = if input.peek(token::Eq) {
input.parse::<token::Eq>()?;
let lit = input.parse::<syn::LitStr>()?;
let default_expr = lit.parse::<syn::Expr>()?;
SpanContainer::new(ident.span(), Some(lit.span()), Some(default_expr))
@ -555,9 +548,9 @@ pub struct FieldAttributes {
pub default: Option<SpanContainer<Option<syn::Expr>>>,
}
impl parse::Parse for FieldAttributes {
fn parse(input: syn::parse::ParseStream) -> syn::parse::Result<Self> {
let items = Punctuated::<FieldAttribute, Token![,]>::parse_terminated(&input)?;
impl Parse for FieldAttributes {
fn parse(input: ParseStream) -> syn::Result<Self> {
let items = Punctuated::<FieldAttribute, token::Comma>::parse_terminated(&input)?;
let mut output = Self::default();
@ -596,7 +589,7 @@ impl FieldAttributes {
pub fn from_attrs(
attrs: &[syn::Attribute],
_mode: FieldAttributeParseMode,
) -> syn::parse::Result<Self> {
) -> syn::Result<Self> {
let doc_comment = get_doc_comment(&attrs);
let deprecation = get_deprecated(&attrs);
@ -669,7 +662,7 @@ pub struct GraphQLTypeDefiniton {
pub description: Option<String>,
pub fields: Vec<GraphQLTypeDefinitionField>,
pub generics: syn::Generics,
pub interfaces: Option<Vec<syn::Type>>,
pub interfaces: Vec<syn::Type>,
// Due to syn parsing differences,
// when parsing an impl the type generics are included in the type
// directly, but in syn::DeriveInput, the type generics are
@ -813,13 +806,17 @@ impl GraphQLTypeDefiniton {
.as_ref()
.map(|description| quote!( .description(#description) ));
let interfaces = self.interfaces.as_ref().map(|items| {
quote!(
let interfaces = if !self.interfaces.is_empty() {
let interfaces_ty = &self.interfaces;
Some(quote!(
.interfaces(&[
#( registry.get_type::< #items >(&()) ,)*
#( registry.get_type::<#interfaces_ty>(&()) ,)*
])
)
});
))
} else {
None
};
// Preserve the original type_generics before modification,
// since alteration makes them invalid if self.generic_scalar
@ -928,7 +925,29 @@ impl GraphQLTypeDefiniton {
.push(parse_quote!( #scalar: Send + Sync ));
where_async.predicates.push(parse_quote!(Self: Sync));
// FIXME: add where clause for interfaces.
let as_dyn_value = if !self.interfaces.is_empty() {
Some(quote! {
#[automatically_derived]
impl#impl_generics ::juniper::AsDynGraphQLValue<#scalar> for #ty #type_generics_tokens
#where_async
{
type Context = <Self as ::juniper::GraphQLValue<#scalar>>::Context;
type TypeInfo = <Self as ::juniper::GraphQLValue<#scalar>>::TypeInfo;
#[inline]
fn as_dyn_graphql_value(&self) -> &::juniper::DynGraphQLValue<#scalar, Self::Context, Self::TypeInfo> {
self
}
#[inline]
fn as_dyn_graphql_value_async(&self) -> &::juniper::DynGraphQLValueAsync<#scalar, Self::Context, Self::TypeInfo> {
self
}
}
})
} else {
None
};
quote!(
impl#impl_generics ::juniper::GraphQLValueAsync<#scalar> for #ty #type_generics_tokens
@ -956,6 +975,8 @@ impl GraphQLTypeDefiniton {
}
}
}
#as_dyn_value
)
};
@ -1144,13 +1165,17 @@ impl GraphQLTypeDefiniton {
.as_ref()
.map(|description| quote!( .description(#description) ));
let interfaces = self.interfaces.as_ref().map(|items| {
quote!(
let interfaces = if !self.interfaces.is_empty() {
let interfaces_ty = &self.interfaces;
Some(quote!(
.interfaces(&[
#( registry.get_type::< #items >(&()) ,)*
#( registry.get_type::<#interfaces_ty>(&()) ,)*
])
)
});
))
} else {
None
};
// Preserve the original type_generics before modification,
// since alteration makes them invalid if self.generic_scalar
@ -1917,6 +1942,7 @@ mod test {
fn test_to_camel_case() {
assert_eq!(&to_camel_case("test")[..], "test");
assert_eq!(&to_camel_case("_test")[..], "test");
assert_eq!(&to_camel_case("__test")[..], "__test");
assert_eq!(&to_camel_case("first_second")[..], "firstSecond");
assert_eq!(&to_camel_case("first_")[..], "first");
assert_eq!(&to_camel_case("a_b_c")[..], "aBC");

View file

@ -1,24 +0,0 @@
/// Handy extension of [`Option`] methods used in this crate.
pub trait OptionExt {
type Inner;
/// Transforms the `Option<T>` into a `Result<(), E>`, mapping `None` to `Ok(())` and `Some(v)`
/// to `Err(err(v))`.
fn none_or_else<E, F>(self, err: F) -> Result<(), E>
where
F: FnOnce(Self::Inner) -> E;
}
impl<T> OptionExt for Option<T> {
type Inner = T;
fn none_or_else<E, F>(self, err: F) -> Result<(), E>
where
F: FnOnce(T) -> E,
{
match self {
Some(v) => Err(err(v)),
None => Ok(()),
}
}
}

View file

@ -5,7 +5,7 @@ use hyper::{
Body, Method, Response, Server, StatusCode,
};
use juniper::{
tests::fixtures::starwars::{model::Database, schema::Query},
tests::fixtures::starwars::schema::{Database, Query},
EmptyMutation, EmptySubscription, RootNode,
};

View file

@ -319,7 +319,7 @@ mod tests {
};
use juniper::{
http::tests as http_tests,
tests::fixtures::starwars::{model::Database, schema::Query},
tests::fixtures::starwars::schema::{Database, Query},
EmptyMutation, EmptySubscription, RootNode,
};
use reqwest::{self, blocking::Response as ReqwestResponse};

Some files were not shown because too many files have changed in this diff Show more