Add subscription support (#433)

Co-authored-by: tyranron <tyranron@gmail.com>
Co-authored-by: Christian Legnitto <LegNeato@users.noreply.github.com>
This commit is contained in:
nWacky 2020-03-19 06:31:36 +03:00 committed by GitHub
parent b7cc424bff
commit eb941e509a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
87 changed files with 4374 additions and 638 deletions

View file

@ -9,12 +9,13 @@ members = [
"juniper_hyper",
"juniper_iron",
"juniper_rocket",
"juniper_subscriptions",
"juniper_warp",
]
exclude = [
"docs/book/tests",
# TODO enable warp
"juniper_warp",
"examples/warp_async",
"examples/warp_subscriptions",
# TODO enable async tests
"juniper_rocket_async",
]

View file

@ -3,12 +3,16 @@ extern crate juniper;
use bencher::Bencher;
use juniper::{execute_sync, RootNode, EmptyMutation, Variables};
use juniper::{execute_sync, RootNode, EmptyMutation, EmptySubscription, Variables};
use juniper::tests::model::Database;
fn query_type_name(b: &mut Bencher) {
let database = Database::new();
let schema = RootNode::new(&database, EmptyMutation::<Database>::new());
let schema = RootNode::new(
&database,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new()
);
let doc = r#"
query IntrospectionQueryTypeQuery {
@ -24,7 +28,11 @@ fn query_type_name(b: &mut Bencher) {
fn introspection_query(b: &mut Bencher) {
let database = Database::new();
let schema = RootNode::new(&database, EmptyMutation::<Database>::new());
let schema = RootNode::new(
&database,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
let doc = r#"
query IntrospectionQuery {

View file

@ -30,7 +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
use juniper::{EmptyMutation, FieldResult, IntrospectionFormat};
use juniper::{EmptyMutation, EmptySubscription, FieldResult, IntrospectionFormat};
// Define our schema.
@ -53,7 +53,12 @@ impl Query {
}
}
type Schema = juniper::RootNode<'static, Query, EmptyMutation<Context>>;
type Schema = juniper::RootNode<
'static,
Query,
EmptyMutation<Context>,
EmptySubscription<Context>
>;
fn main() {
// Create a context object.
@ -61,7 +66,7 @@ fn main() {
// Run the built-in introspection query.
let (res, _errors) = juniper::introspect(
&Schema::new(Query, EmptyMutation::new()),
&Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()),
&ctx,
IntrospectionFormat::default(),
).unwrap();

View file

@ -24,7 +24,7 @@ types to a GraphQL schema. The most important one is the
resolvers, which you will use for the `Query` and `Mutation` roots.
```rust
use juniper::{FieldResult};
use juniper::{FieldResult, EmptySubscription};
# struct DatabasePool;
# impl DatabasePool {
@ -119,10 +119,10 @@ impl Mutation {
// A root schema consists of a query and a mutation.
// Request queries can be executed against a RootNode.
type Schema = juniper::RootNode<'static, Query, Mutation>;
type Schema = juniper::RootNode<'static, Query, Mutation, EmptySubscription<Context>>;
# fn main() {
# let _ = Schema::new(Query, Mutation{});
# let _ = Schema::new(Query, Mutation{}, EmptySubscription::new());
# }
```
@ -139,7 +139,7 @@ You can invoke `juniper::execute` directly to run a GraphQL query:
```rust
# // Only needed due to 2018 edition because the macro is not accessible.
# #[macro_use] extern crate juniper;
use juniper::{FieldResult, Variables, EmptyMutation};
use juniper::{FieldResult, Variables, EmptyMutation, EmptySubscription};
#[derive(juniper::GraphQLEnum, Clone, Copy)]
@ -168,7 +168,7 @@ impl Query {
// A root schema consists of a query and a mutation.
// Request queries can be executed against a RootNode.
type Schema = juniper::RootNode<'static, Query, EmptyMutation<Ctx>>;
type Schema = juniper::RootNode<'static, Query, EmptyMutation<Ctx>, EmptySubscription<Ctx>>;
fn main() {
// Create a context object.
@ -178,7 +178,7 @@ fn main() {
let (res, _errors) = juniper::execute_sync(
"query { favoriteEpisode }",
None,
&Schema::new(Query, EmptyMutation::new()),
&Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()),
&Variables::new(),
&ctx,
).unwrap();

View file

@ -12,6 +12,7 @@ env_logger = "0.6.2"
warp = "0.1.19"
futures = { version = "0.3.1", features = ["compat"] }
reqwest = "0.9.19"
tokio = { version = "0.2", features = ["rt-core", "macros"] }
juniper_codegen = { git = "https://github.com/graphql-rust/juniper", branch = "async-await", features = ["async"] }
juniper = { git = "https://github.com/graphql-rust/juniper", branch = "async-await", features = ["async"] }

View file

@ -2,7 +2,7 @@
//! This example demonstrates async/await usage with warp.
//! NOTE: this uses tokio 0.1 , not the alpha tokio 0.2.
use juniper::{EmptyMutation, RootNode, FieldError};
use juniper::{EmptyMutation, EmptySubscription, RootNode, FieldError};
use warp::{http::Response, Filter};
#[derive(Clone)]
@ -73,13 +73,14 @@ impl Query {
}
}
type Schema = RootNode<'static, Query, EmptyMutation<Context>>;
type Schema = RootNode<'static, Query, EmptyMutation<Context>, EmptySubscription<Context>>;
fn schema() -> Schema {
Schema::new(Query, EmptyMutation::<Context>::new())
Schema::new(Query, EmptyMutation::<Context>::new(), EmptySubscription::<Context>::new())
}
fn main() {
#[tokio::main]
async fn main() {
::std::env::set_var("RUST_LOG", "warp_async");
env_logger::init();
@ -96,15 +97,16 @@ fn main() {
log::info!("Listening on 127.0.0.1:8080");
let state = warp::any().map(move || Context{} );
let graphql_filter = juniper_warp::make_graphql_filter_async(schema(), state.boxed());
let graphql_filter = juniper_warp::make_graphql_filter(schema(), state.boxed());
warp::serve(
warp::get2()
warp::get()
.and(warp::path("graphiql"))
.and(juniper_warp::graphiql_filter("/graphql"))
.or(homepage)
.or(warp::path("graphql").and(graphql_filter))
.with(log),
)
.run(([127, 0, 0, 1], 8080));
.run(([127, 0, 0, 1], 8080))
.await
}

View file

@ -0,0 +1 @@
target

View file

@ -0,0 +1,20 @@
[package]
name = "warp_subscriptions"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
env_logger = "0.6.2"
futures = { version = "=0.3.1" }
log = "0.4.8"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "0.2", features = ["rt-core", "macros"] }
warp = "0.2.1"
# TODO#433: get crates from GitHub
juniper = { path = "../../juniper" }
juniper_subscriptions = { path = "../../juniper_subscriptions"}
juniper_warp = { path = "../../juniper_warp", features = ["subscriptions"] }

View file

@ -0,0 +1,184 @@
//! This example demonstrates asynchronous subscriptions with warp and tokio 0.2
use std::{pin::Pin, sync::Arc, time::Duration};
use futures::{Future, FutureExt as _, Stream};
use juniper::{DefaultScalarValue, EmptyMutation, FieldError, RootNode};
use juniper_subscriptions::Coordinator;
use juniper_warp::{playground_filter, subscriptions::graphql_subscriptions};
use warp::{http::Response, Filter};
#[derive(Clone)]
struct Context {}
impl juniper::Context for Context {}
#[derive(Clone, Copy, juniper::GraphQLEnum)]
enum UserKind {
Admin,
User,
Guest,
}
struct User {
id: i32,
kind: UserKind,
name: String,
}
// Field resolvers implementation
#[juniper::graphql_object(Context = Context)]
impl User {
fn id(&self) -> i32 {
self.id
}
fn kind(&self) -> UserKind {
self.kind
}
fn name(&self) -> &str {
&self.name
}
async fn friends(&self) -> Vec<User> {
if self.id == 1 {
return vec![
User {
id: 11,
kind: UserKind::User,
name: "user11".into(),
},
User {
id: 12,
kind: UserKind::Admin,
name: "user12".into(),
},
User {
id: 13,
kind: UserKind::Guest,
name: "user13".into(),
},
];
} else if self.id == 2 {
return vec![User {
id: 21,
kind: UserKind::User,
name: "user21".into(),
}];
} else if self.id == 3 {
return vec![
User {
id: 31,
kind: UserKind::User,
name: "user31".into(),
},
User {
id: 32,
kind: UserKind::Guest,
name: "user32".into(),
},
];
} else {
return vec![];
}
}
}
struct Query;
#[juniper::graphql_object(Context = Context)]
impl Query {
async fn users(id: i32) -> Vec<User> {
vec![User {
id,
kind: UserKind::Admin,
name: "User Name".into(),
}]
}
}
type UsersStream = Pin<Box<dyn Stream<Item = Result<User, FieldError>> + Send>>;
struct Subscription;
#[juniper::graphql_subscription(Context = Context)]
impl Subscription {
async fn users() -> UsersStream {
let mut counter = 0;
let stream = tokio::time::interval(Duration::from_secs(5)).map(move |_| {
counter += 1;
if counter == 2 {
Err(FieldError::new(
"some field error from handler",
Value::Scalar(DefaultScalarValue::String(
"some additional string".to_string(),
)),
))
} else {
Ok(User {
id: counter,
kind: UserKind::Admin,
name: "stream user".to_string(),
})
}
});
Box::pin(stream)
}
}
type Schema = RootNode<'static, Query, EmptyMutation<Context>, Subscription>;
fn schema() -> Schema {
Schema::new(Query, EmptyMutation::new(), Subscription)
}
#[tokio::main]
async fn main() {
::std::env::set_var("RUST_LOG", "warp_subscriptions");
env_logger::init();
let log = warp::log("warp_server");
let homepage = warp::path::end().map(|| {
Response::builder()
.header("content-type", "text/html")
.body(format!(
"<html><h1>juniper_subscriptions demo</h1><div>visit <a href=\"/playground\">graphql playground</a></html>"
))
});
let qm_schema = schema();
let qm_state = warp::any().map(move || Context {});
let qm_graphql_filter = juniper_warp::make_graphql_filter(qm_schema, qm_state.boxed());
let sub_state = warp::any().map(move || Context {});
let coordinator = Arc::new(juniper_subscriptions::Coordinator::new(schema()));
log::info!("Listening on 127.0.0.1:8080");
let routes = (warp::path("subscriptions")
.and(warp::ws())
.and(sub_state.clone())
.and(warp::any().map(move || Arc::clone(&coordinator)))
.map(
|ws: warp::ws::Ws,
ctx: Context,
coordinator: Arc<Coordinator<'static, _, _, _, _, _>>| {
ws.on_upgrade(|websocket| -> Pin<Box<dyn Future<Output = ()> + Send>> {
graphql_subscriptions(websocket, coordinator, ctx).boxed()
})
},
))
.or(warp::post()
.and(warp::path("graphql"))
.and(qm_graphql_filter))
.or(warp::get()
.and(warp::path("playground"))
.and(playground_filter("/graphql", Some("/subscriptions"))))
.or(homepage)
.with(log);
warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
}

View file

@ -75,9 +75,14 @@ struct Mutation;
#[juniper::graphql_object]
impl Mutation {}
struct Subscription;
#[juniper::graphql_subscription]
impl Subscription {}
#[tokio::test]
async fn async_simple() {
let schema = RootNode::new(Query, Mutation);
let schema = RootNode::new(Query, Mutation, Subscription);
let doc = r#"
query {
fieldSync
@ -119,7 +124,7 @@ async fn async_simple() {
#[tokio::test]
async fn async_field_validation_error() {
let schema = RootNode::new(Query, Mutation);
let schema = RootNode::new(Query, Mutation, Subscription);
let doc = r#"
query {
nonExistentField

View file

@ -5,7 +5,9 @@ use juniper::Object;
use juniper::{DefaultScalarValue, GraphQLObject};
#[cfg(test)]
use juniper::{self, execute, EmptyMutation, GraphQLType, RootNode, Value, Variables};
use juniper::{
self, execute, EmptyMutation, EmptySubscription, GraphQLType, RootNode, Value, Variables,
};
use futures;
@ -195,7 +197,11 @@ async fn test_derived_object() {
}
}"#;
let schema = RootNode::new(Query, EmptyMutation::<()>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
assert_eq!(
execute(doc, None, &schema, &Variables::new(), &()).await,
@ -229,7 +235,11 @@ async fn test_cannot_query_skipped_field() {
skippedField
}
}"#;
let schema = RootNode::new(Query, EmptyMutation::<()>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
execute(doc, None, &schema, &Variables::new(), &())
.await
.unwrap();
@ -243,7 +253,11 @@ async fn test_skipped_field_siblings_unaffected() {
regularField
}
}"#;
let schema = RootNode::new(Query, EmptyMutation::<()>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
execute(doc, None, &schema, &Variables::new(), &())
.await
.unwrap();
@ -261,7 +275,11 @@ async fn test_derived_object_nested() {
}
}"#;
let schema = RootNode::new(Query, EmptyMutation::<()>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
assert_eq!(
execute(doc, None, &schema, &Variables::new(), &()).await,
@ -341,7 +359,11 @@ async fn run_type_info_query<F>(doc: &str, f: F)
where
F: Fn((&Object<DefaultScalarValue>, &Vec<Value>)) -> (),
{
let schema = RootNode::new(Query, EmptyMutation::<()>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let (result, errs) = execute(doc, None, &schema, &Variables::new(), &())
.await

View file

@ -1,6 +1,7 @@
#[cfg(test)]
use juniper::{
self, execute, graphql_value, EmptyMutation, GraphQLInputObject, RootNode, Value, Variables,
self, execute, graphql_value, EmptyMutation, EmptySubscription, GraphQLInputObject, RootNode,
Value, Variables,
};
use futures;
@ -90,7 +91,11 @@ async fn supports_raw_idents_in_fields_of_input_types() {
#[cfg(test)]
async fn run_type_info_query(doc: &str) -> Value {
let schema = RootNode::new(Query, EmptyMutation::<()>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let (result, errs) = execute(doc, None, &schema, &Variables::new(), &())
.await

View file

@ -6,7 +6,8 @@ use juniper::{
execute,
parser::{ParseError, ScalarToken, Spanning, Token},
serde::de,
EmptyMutation, InputValue, Object, ParseScalarResult, RootNode, ScalarValue, Value, Variables,
EmptyMutation, EmptySubscription, InputValue, Object, ParseScalarResult, RootNode, ScalarValue,
Value, Variables,
};
use std::fmt;
@ -176,7 +177,11 @@ async fn run_variable_query<F>(query: &str, vars: Variables<MyScalarValue>, f: F
where
F: Fn(&Object<MyScalarValue>) -> (),
{
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let (result, errs) = execute(query, None, &schema, &vars, &())
.await

View file

@ -48,7 +48,7 @@ impl Country {
}
}
type Schema = juniper::RootNode<'static, Query, EmptyMutation<Context>>;
type Schema = juniper::RootNode<'static, Query, EmptyMutation<Context>, EmptySubscription<Context>>;
#[tokio::test]
async fn users() {
@ -59,7 +59,11 @@ async fn users() {
let (_, errors) = juniper::execute(
query,
None,
&Schema::new(Query, EmptyMutation::<Context>::new()),
&Schema::new(
Query,
EmptyMutation::<Context>::new(),
EmptySubscription::<Context>::new(),
),
&juniper::Variables::new(),
&ctx,
)
@ -78,7 +82,7 @@ async fn countries() {
let (_, errors) = juniper::execute(
query,
None,
&Schema::new(Query, EmptyMutation::new()),
&Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()),
&juniper::Variables::new(),
&ctx,
)
@ -102,7 +106,11 @@ async fn both() {
let (_, errors) = juniper::execute(
query,
None,
&Schema::new(Query, EmptyMutation::<Context>::new()),
&Schema::new(
Query,
EmptyMutation::<Context>::new(),
EmptySubscription::<Context>::new(),
),
&juniper::Variables::new(),
&ctx,
)
@ -126,7 +134,11 @@ async fn both_in_different_order() {
let (_, errors) = juniper::execute(
query,
None,
&Schema::new(Query, EmptyMutation::<Context>::new()),
&Schema::new(
Query,
EmptyMutation::<Context>::new(),
EmptySubscription::<Context>::new(),
),
&juniper::Variables::new(),
&ctx,
)

View file

@ -42,7 +42,7 @@ impl Country {
}
}
type Schema = juniper::RootNode<'static, Query, EmptyMutation<()>>;
type Schema = juniper::RootNode<'static, Query, EmptyMutation<()>, EmptySubscription<()>>;
#[tokio::test]
async fn test_lookahead_from_fragment_with_nested_type() {
@ -61,7 +61,7 @@ async fn test_lookahead_from_fragment_with_nested_type() {
}
"#,
None,
&Schema::new(Query, EmptyMutation::new()),
&Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()),
&Variables::new(),
&(),
)

View file

@ -9,6 +9,9 @@
- `LexerError`
- `ParseError`
- `RuleError`
- Support subscriptions (see
[#433](https://github.com/graphql-rust/juniper/pull/433) for more details)
See [#419](https://github.com/graphql-rust/juniper/pull/419).
@ -25,13 +28,17 @@ See [#569](https://github.com/graphql-rust/juniper/pull/569).
- `graphql_union!` macro removed, replaced by `#[graphql_union]` proc macro
- ScalarRefValue trait removed
- ScalarRefValue trait removed
Trait was not required.
- Changed return type of GraphQLType::resolve to `ExecutionResult`
This was done to unify the return type of all resolver methods
The previous `Value` return type was just an internal artifact of
error handling.
- Subscription-related:
add subscription type to `RootNode`,
add subscription endpoint to `playground_source()`
# [[0.14.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper-0.14.2)

View file

@ -25,5 +25,6 @@ pre-release-replacements = [
# Warp
{file="../juniper_warp/Cargo.toml", search="juniper = \\{ version = \"[^\"]+\"", replace="juniper = { version = \"{{version}}\""},
{file="../juniper_warp/Cargo.toml", search="\\[dev-dependencies\\.juniper\\]\nversion = \"[^\"]+\"", replace="[dev-dependencies.juniper]\nversion = \"{{version}}\""},
# Subscriptions
{file="../juniper_subscriptions/Cargo.toml", search="juniper = \\{ version = \"[^\"]+\"", replace="juniper = { version = \"{{version}}\""},
]

View file

@ -185,7 +185,7 @@ where
pub(super) fn build_from_selection(
s: &'a Selection<'a, S>,
vars: &'a Variables<S>,
fragments: &'a HashMap<&'a str, &'a Fragment<'a, S>>,
fragments: &'a HashMap<&'a str, Fragment<'a, S>>,
) -> Option<LookAheadSelection<'a, S>> {
Self::build_from_selection_with_parent(s, None, vars, fragments)
}
@ -194,7 +194,7 @@ where
s: &'a Selection<'a, S>,
parent: Option<&mut Self>,
vars: &'a Variables<S>,
fragments: &'a HashMap<&'a str, &'a Fragment<'a, S>>,
fragments: &'a HashMap<&'a str, Fragment<'a, S>>,
) -> Option<LookAheadSelection<'a, S>> {
let empty: &[Selection<S>] = &[];
match *s {
@ -429,7 +429,7 @@ mod tests {
ast::Document,
parser::UnlocatedParseResult,
schema::model::SchemaType,
validation::test_harness::{MutationRoot, QueryRoot},
validation::test_harness::{MutationRoot, QueryRoot, SubscriptionRoot},
value::{DefaultScalarValue, ScalarValue},
};
use std::collections::HashMap;
@ -438,14 +438,20 @@ mod tests {
where
S: ScalarValue,
{
crate::parse_document_source(q, &SchemaType::new::<QueryRoot, MutationRoot>(&(), &()))
crate::parse_document_source(
q,
&SchemaType::new::<QueryRoot, MutationRoot, SubscriptionRoot>(&(), &(), &()),
)
}
fn extract_fragments<'a, S>(doc: &'a Document<S>) -> HashMap<&'a str, &'a Fragment<'a, S>> {
fn extract_fragments<'a, S>(doc: &'a Document<S>) -> HashMap<&'a str, Fragment<'a, S>>
where
S: Clone,
{
let mut fragments = HashMap::new();
for d in doc {
if let crate::ast::Definition::Fragment(ref f) = *d {
let f = &f.item;
let f = f.item.clone();
fragments.insert(f.name.item, f);
}
}

View file

@ -3,7 +3,7 @@ use std::{
cmp::Ordering,
collections::HashMap,
fmt::{Debug, Display},
sync::RwLock,
sync::{Arc, RwLock},
};
use fnv::FnvHashMap;
@ -13,31 +13,30 @@ use crate::{
Definition, Document, Fragment, FromInputValue, InputValue, Operation, OperationType,
Selection, ToInputValue, Type,
},
parser::SourcePosition,
value::Value,
parser::{SourcePosition, Spanning},
schema::{
meta::{
Argument, DeprecationStatus, EnumMeta, EnumValue, Field, InputObjectMeta,
InterfaceMeta, ListMeta, MetaType, NullableMeta, ObjectMeta, PlaceholderMeta,
ScalarMeta, UnionMeta,
},
model::{RootNode, SchemaType, TypeType},
},
types::{base::GraphQLType, name::Name},
value::{DefaultScalarValue, ParseScalarValue, ScalarValue, Value},
GraphQLError,
};
use crate::schema::{
meta::{
Argument, DeprecationStatus, EnumMeta, EnumValue, Field, InputObjectMeta, InterfaceMeta,
ListMeta, MetaType, NullableMeta, ObjectMeta, PlaceholderMeta, ScalarMeta, UnionMeta,
pub use self::{
look_ahead::{
Applies, ChildSelection, ConcreteLookAheadSelection, LookAheadArgument, LookAheadMethods,
LookAheadSelection, LookAheadValue,
},
model::{RootNode, SchemaType, TypeType},
};
use crate::{
types::{base::GraphQLType, name::Name},
value::{DefaultScalarValue, ParseScalarValue, ScalarValue},
owned_executor::OwnedExecutor,
};
mod look_ahead;
pub use self::look_ahead::{
Applies, ChildSelection, ConcreteLookAheadSelection, LookAheadArgument, LookAheadMethods,
LookAheadSelection, LookAheadValue,
};
use crate::parser::Spanning;
mod owned_executor;
/// A type registry used to build schemas
///
@ -52,27 +51,27 @@ pub struct Registry<'r, S = DefaultScalarValue> {
#[derive(Clone)]
pub enum FieldPath<'a> {
Root(SourcePosition),
Field(&'a str, SourcePosition, &'a FieldPath<'a>),
Field(&'a str, SourcePosition, Arc<FieldPath<'a>>),
}
/// Query execution engine
///
/// The executor helps drive the query execution in a schema. It keeps track
/// of the current field stack, context, variables, and errors.
pub struct Executor<'a, CtxT, S = DefaultScalarValue>
pub struct Executor<'r, 'a, CtxT, S = DefaultScalarValue>
where
CtxT: 'a,
S: 'a,
{
fragments: &'a HashMap<&'a str, &'a Fragment<'a, S>>,
variables: &'a Variables<S>,
current_selection_set: Option<&'a [Selection<'a, S>]>,
parent_selection_set: Option<&'a [Selection<'a, S>]>,
fragments: &'r HashMap<&'a str, Fragment<'a, S>>,
variables: &'r Variables<S>,
current_selection_set: Option<&'r [Selection<'a, S>]>,
parent_selection_set: Option<&'r [Selection<'a, S>]>,
current_type: TypeType<'a, S>,
schema: &'a SchemaType<'a, S>,
context: &'a CtxT,
errors: &'a RwLock<Vec<ExecutionError<S>>>,
field_path: FieldPath<'a>,
errors: &'r RwLock<Vec<ExecutionError<S>>>,
field_path: Arc<FieldPath<'a>>,
}
/// Error type for errors that occur during query execution
@ -220,6 +219,10 @@ pub type FieldResult<T, S = DefaultScalarValue> = Result<T, FieldError<S>>;
/// The result of resolving an unspecified field
pub type ExecutionResult<S = DefaultScalarValue> = Result<Value<S>, FieldError<S>>;
/// Boxed `futures::Stream` yielding `Result<Value<S>, ExecutionError<S>>`
pub type ValuesStream<'a, S = DefaultScalarValue> =
std::pin::Pin<Box<dyn futures::Stream<Item = Result<Value<S>, ExecutionError<S>>> + Send + 'a>>;
/// The map of variables used for substitution during query execution
pub type Variables<S = DefaultScalarValue> = HashMap<String, InputValue<S>>;
@ -349,10 +352,54 @@ where
}
}
impl<'a, CtxT, S> Executor<'a, CtxT, S>
impl<'r, 'a, CtxT, S> Executor<'r, 'a, CtxT, S>
where
S: ScalarValue,
{
/// Resolve a single arbitrary value into a stream of [`Value`]s.
/// If a field fails to resolve, pushes error to `Executor`
/// and returns `Value::Null`.
pub async fn resolve_into_stream<'i, 'v, 'res, T>(
&'r self,
info: &'i T::TypeInfo,
value: &'v T,
) -> Value<ValuesStream<'res, S>>
where
'i: 'res,
'v: 'res,
'a: 'res,
T: crate::GraphQLSubscriptionType<S, Context = CtxT> + Send + Sync,
T::TypeInfo: Send + Sync,
CtxT: Send + Sync,
S: Send + Sync,
{
match self.subscribe(info, value).await {
Ok(v) => v,
Err(e) => {
self.push_error(e);
Value::Null
}
}
}
/// Resolve a single arbitrary value into a stream of [`Value`]s.
/// Calls `resolve_into_stream` on `T`.
pub async fn subscribe<'s, 't, 'res, T>(
&'r self,
info: &'t T::TypeInfo,
value: &'t T,
) -> Result<Value<ValuesStream<'res, S>>, FieldError<S>>
where
't: 'res,
'a: 'res,
T: crate::GraphQLSubscriptionType<S, Context = CtxT>,
T::TypeInfo: Send + Sync,
CtxT: Send + Sync,
S: Send + Sync,
{
value.resolve_into_stream(info, self).await
}
/// Resolve a single arbitrary value, mapping the context to a new type
pub fn resolve_with_ctx<NewCtxT, T>(&self, info: &T::TypeInfo, value: &T) -> ExecutionResult<S>
where
@ -439,7 +486,10 @@ where
///
/// This can be used to connect different types, e.g. from different Rust
/// libraries, that require different context types.
pub fn replaced_context<'b, NewCtxT>(&'b self, ctx: &'b NewCtxT) -> Executor<'b, NewCtxT, S> {
pub fn replaced_context<'b, NewCtxT>(
&'b self,
ctx: &'b NewCtxT,
) -> Executor<'b, 'b, NewCtxT, S> {
Executor {
fragments: self.fragments,
variables: self.variables,
@ -454,13 +504,13 @@ where
}
#[doc(hidden)]
pub fn field_sub_executor(
&self,
pub fn field_sub_executor<'s>(
&'s self,
field_alias: &'a str,
field_name: &'a str,
field_name: &'s str,
location: SourcePosition,
selection_set: Option<&'a [Selection<S>]>,
) -> Executor<CtxT, S> {
selection_set: Option<&'s [Selection<'a, S>]>,
) -> Executor<'s, 'a, CtxT, S> {
Executor {
fragments: self.fragments,
variables: self.variables,
@ -477,16 +527,20 @@ where
schema: self.schema,
context: self.context,
errors: self.errors,
field_path: FieldPath::Field(field_alias, location, &self.field_path),
field_path: Arc::new(FieldPath::Field(
field_alias,
location,
Arc::clone(&self.field_path),
)),
}
}
#[doc(hidden)]
pub fn type_sub_executor(
&self,
type_name: Option<&'a str>,
selection_set: Option<&'a [Selection<S>]>,
) -> Executor<CtxT, S> {
pub fn type_sub_executor<'s>(
&'s self,
type_name: Option<&'s str>,
selection_set: Option<&'s [Selection<'a, S>]>,
) -> Executor<'s, 'a, CtxT, S> {
Executor {
fragments: self.fragments,
variables: self.variables,
@ -503,11 +557,16 @@ where
}
}
/// `Executor`'s current selection set
pub(crate) fn current_selection_set(&self) -> Option<&[Selection<'a, S>]> {
self.current_selection_set
}
/// Access the current context
///
/// You usually provide the context when calling the top-level `execute`
/// function, or using the context factory in the Iron integration.
pub fn context(&self) -> &'a CtxT {
pub fn context(&self) -> &'r CtxT {
self.context
}
@ -522,13 +581,13 @@ where
}
#[doc(hidden)]
pub fn variables(&self) -> &'a Variables<S> {
pub fn variables(&self) -> &'r Variables<S> {
self.variables
}
#[doc(hidden)]
pub fn fragment_by_name(&'a self, name: &str) -> Option<&'a Fragment<'a, S>> {
self.fragments.get(name).cloned()
pub fn fragment_by_name<'s>(&'s self, name: &str) -> Option<&'s Fragment<'a, S>> {
self.fragments.get(name)
}
/// The current location of the executor
@ -555,12 +614,24 @@ where
});
}
/// Returns new [`ExecutionError`] at current location
pub fn new_error(&self, error: FieldError<S>) -> ExecutionError<S> {
let mut path = Vec::new();
self.field_path.construct_path(&mut path);
ExecutionError {
location: self.location().clone(),
path,
error,
}
}
/// Construct a lookahead selection for the current selection.
///
/// This allows seeing the whole selection and perform operations
/// affecting the children.
pub fn look_ahead(&'a self) -> LookAheadSelection<'a, S> {
let field_name = match self.field_path {
let field_name = match *self.field_path {
FieldPath::Field(x, ..) => x,
FieldPath::Root(_) => unreachable!(),
};
@ -596,7 +667,7 @@ where
s.iter()
.map(|s| ChildSelection {
inner: LookAheadSelection::build_from_selection(
s,
&s,
self.variables,
self.fragments,
)
@ -610,15 +681,36 @@ where
})
.unwrap_or_default()
}
/// Create new `OwnedExecutor` and clone all current data
/// (except for errors) there
///
/// New empty vector is created for `errors` because
/// existing errors won't be needed to be accessed by user
/// in OwnedExecutor as existing errors will be returned in
/// `execute_query`/`execute_mutation`/`resolve_into_stream`/etc.
pub fn as_owned_executor(&self) -> OwnedExecutor<'a, CtxT, S> {
OwnedExecutor {
fragments: self.fragments.clone(),
variables: self.variables.clone(),
current_selection_set: self.current_selection_set.map(|x| x.to_vec()),
parent_selection_set: self.parent_selection_set.map(|x| x.to_vec()),
current_type: self.current_type.clone(),
schema: self.schema,
context: self.context,
errors: RwLock::new(vec![]),
field_path: Arc::clone(&self.field_path),
}
}
}
impl<'a> FieldPath<'a> {
fn construct_path(&self, acc: &mut Vec<String>) {
match *self {
match self {
FieldPath::Root(_) => (),
FieldPath::Field(name, _, parent) => {
parent.construct_path(acc);
acc.push(name.to_owned());
acc.push((*name).to_owned());
}
}
}
@ -656,10 +748,12 @@ impl<S> ExecutionError<S> {
}
}
pub fn execute_validated_query<'a, 'b, QueryT, MutationT, CtxT, S>(
/// Create new `Executor` and start query/mutation execution.
/// Returns `IsSubscription` error if subscription is passed.
pub fn execute_validated_query<'a, 'b, QueryT, MutationT, SubscriptionT, CtxT, S>(
document: &'b Document<S>,
operation: &'b Spanning<Operation<S>>,
root_node: &RootNode<QueryT, MutationT, S>,
root_node: &RootNode<QueryT, MutationT, SubscriptionT, S>,
variables: &Variables<S>,
context: &CtxT,
) -> Result<(Value<S>, Vec<ExecutionError<S>>), GraphQLError<'a>>
@ -667,7 +761,12 @@ where
S: ScalarValue,
QueryT: GraphQLType<S, Context = CtxT>,
MutationT: GraphQLType<S, Context = CtxT>,
SubscriptionT: GraphQLType<S, Context = CtxT>,
{
if operation.item.operation_type == OperationType::Subscription {
return Err(GraphQLError::IsSubscription);
}
let mut fragments = vec![];
for def in document.iter() {
if let Definition::Fragment(f) = def {
@ -716,7 +815,7 @@ where
let executor = Executor {
fragments: &fragments
.iter()
.map(|f| (f.item.name.item, &f.item))
.map(|f| (f.item.name.item, f.item.clone()))
.collect(),
variables: final_vars,
current_selection_set: Some(&operation.item.selection_set[..]),
@ -725,7 +824,7 @@ where
schema: &root_node.schema,
context,
errors: &errors,
field_path: FieldPath::Root(operation.start),
field_path: Arc::new(FieldPath::Root(operation.start)),
};
value = match operation.item.operation_type {
@ -743,10 +842,12 @@ where
Ok((value, errors))
}
pub async fn execute_validated_query_async<'a, 'b, QueryT, MutationT, CtxT, S>(
/// Create new `Executor` and start asynchronous query execution.
/// Returns `IsSubscription` error if subscription is passed.
pub async fn execute_validated_query_async<'a, 'b, QueryT, MutationT, SubscriptionT, CtxT, S>(
document: &'b Document<'a, S>,
operation: &'b Spanning<Operation<'_, S>>,
root_node: &RootNode<'a, QueryT, MutationT, S>,
root_node: &RootNode<'a, QueryT, MutationT, SubscriptionT, S>,
variables: &Variables<S>,
context: &CtxT,
) -> Result<(Value<S>, Vec<ExecutionError<S>>), GraphQLError<'a>>
@ -756,8 +857,14 @@ where
QueryT::TypeInfo: Send + Sync,
MutationT: crate::GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
MutationT::TypeInfo: Send + Sync,
SubscriptionT: GraphQLType<S, Context = CtxT> + Send + Sync,
SubscriptionT::TypeInfo: Send + Sync,
CtxT: Send + Sync,
{
if operation.item.operation_type == OperationType::Subscription {
return Err(GraphQLError::IsSubscription);
}
let mut fragments = vec![];
for def in document.iter() {
if let Definition::Fragment(f) = def {
@ -806,7 +913,7 @@ where
let executor = Executor {
fragments: &fragments
.iter()
.map(|f| (f.item.name.item, &f.item))
.map(|f| (f.item.name.item, f.item.clone()))
.collect(),
variables: final_vars,
current_selection_set: Some(&operation.item.selection_set[..]),
@ -815,7 +922,7 @@ where
schema: &root_node.schema,
context,
errors: &errors,
field_path: FieldPath::Root(operation.start),
field_path: Arc::new(FieldPath::Root(operation.start)),
};
value = match operation.item.operation_type {
@ -839,10 +946,10 @@ where
Ok((value, errors))
}
pub fn get_operation<'a, 'b, S>(
document: &'b Document<'b, S>,
pub fn get_operation<'b, 'd, 'e, S>(
document: &'b Document<'d, S>,
operation_name: Option<&str>,
) -> Result<&'b Spanning<Operation<'b, S>>, GraphQLError<'a>>
) -> Result<&'b Spanning<Operation<'d, S>>, GraphQLError<'e>>
where
S: ScalarValue,
{
@ -865,12 +972,122 @@ where
Some(op) => op,
None => return Err(GraphQLError::UnknownOperationName),
};
if op.item.operation_type == OperationType::Subscription {
return Err(GraphQLError::IsSubscription);
}
Ok(op)
}
/// Initialize new `Executor` and start resolving subscription into stream
/// asynchronously.
/// Returns `NotSubscription` error if query or mutation is passed
pub async fn resolve_validated_subscription<
'r,
'exec_ref,
'd,
'op,
QueryT,
MutationT,
SubscriptionT,
CtxT,
S,
>(
document: &Document<'d, S>,
operation: &Spanning<Operation<'op, S>>,
root_node: &'r RootNode<'r, QueryT, MutationT, SubscriptionT, S>,
variables: &Variables<S>,
context: &'r CtxT,
) -> Result<(Value<ValuesStream<'r, S>>, Vec<ExecutionError<S>>), GraphQLError<'r>>
where
'r: 'exec_ref,
'd: 'r,
'op: 'd,
S: ScalarValue + Send + Sync,
QueryT: crate::GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
QueryT::TypeInfo: Send + Sync,
MutationT: crate::GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
MutationT::TypeInfo: Send + Sync,
SubscriptionT: crate::GraphQLSubscriptionType<S, Context = CtxT> + Send + Sync,
SubscriptionT::TypeInfo: Send + Sync,
CtxT: Send + Sync + 'r,
{
if operation.item.operation_type != OperationType::Subscription {
return Err(GraphQLError::NotSubscription);
}
let mut fragments = vec![];
for def in document.iter() {
match def {
Definition::Fragment(f) => fragments.push(f),
_ => (),
};
}
let default_variable_values = operation.item.variable_definitions.as_ref().map(|defs| {
defs.item
.items
.iter()
.filter_map(|&(ref name, ref def)| {
def.default_value
.as_ref()
.map(|i| (name.item.to_owned(), i.item.clone()))
})
.collect::<HashMap<String, InputValue<S>>>()
});
let errors = RwLock::new(Vec::new());
let value;
{
let mut all_vars;
let mut final_vars = variables;
if let Some(defaults) = default_variable_values {
all_vars = variables.clone();
for (name, value) in defaults {
all_vars.entry(name).or_insert(value);
}
final_vars = &all_vars;
}
let root_type = match operation.item.operation_type {
OperationType::Subscription => root_node
.schema
.subscription_type()
.expect("No subscription type found"),
_ => unreachable!(),
};
let executor: Executor<'_, 'r, _, _> = Executor {
fragments: &fragments
.iter()
.map(|f| (f.item.name.item, f.item.clone()))
.collect(),
variables: final_vars,
current_selection_set: Some(&operation.item.selection_set[..]),
parent_selection_set: None,
current_type: root_type,
schema: &root_node.schema,
context,
errors: &errors,
field_path: Arc::new(FieldPath::Root(operation.start)),
};
value = match operation.item.operation_type {
OperationType::Subscription => {
executor
.resolve_into_stream(&root_node.subscription_info, &root_node.subscription_type)
.await
}
_ => unreachable!(),
};
}
let mut errors = errors.into_inner().unwrap();
errors.sort();
Ok((value, errors))
}
impl<'r, S> Registry<'r, S>
where
S: ScalarValue + 'r,

View file

@ -0,0 +1,154 @@
use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
use crate::{
ast::Fragment,
executor::FieldPath,
parser::SourcePosition,
schema::model::{SchemaType, TypeType},
ExecutionError, Executor, Selection, Variables,
};
/// [`Executor`] owning all its variables. Can be used after [`Executor`] was
/// destroyed.
pub struct OwnedExecutor<'a, CtxT, S> {
pub(super) fragments: HashMap<&'a str, Fragment<'a, S>>,
pub(super) variables: Variables<S>,
pub(super) current_selection_set: Option<Vec<Selection<'a, S>>>,
pub(super) parent_selection_set: Option<Vec<Selection<'a, S>>>,
pub(super) current_type: TypeType<'a, S>,
pub(super) schema: &'a SchemaType<'a, S>,
pub(super) context: &'a CtxT,
pub(super) errors: RwLock<Vec<ExecutionError<S>>>,
pub(super) field_path: Arc<FieldPath<'a>>,
}
impl<'a, CtxT, S> Clone for OwnedExecutor<'a, CtxT, S>
where
S: Clone,
{
fn clone(&self) -> Self {
Self {
fragments: self.fragments.clone(),
variables: self.variables.clone(),
current_selection_set: self.current_selection_set.clone(),
parent_selection_set: self.parent_selection_set.clone(),
current_type: self.current_type.clone(),
schema: self.schema,
context: self.context,
errors: RwLock::new(vec![]),
field_path: self.field_path.clone(),
}
}
}
impl<'a, CtxT, S> OwnedExecutor<'a, CtxT, S>
where
S: Clone,
{
#[doc(hidden)]
pub fn type_sub_executor(
&self,
type_name: Option<&str>,
selection_set: Option<Vec<Selection<'a, S>>>,
) -> OwnedExecutor<'a, CtxT, S> {
OwnedExecutor {
fragments: self.fragments.clone(),
variables: self.variables.clone(),
current_selection_set: selection_set,
parent_selection_set: self.current_selection_set.clone(),
current_type: match type_name {
Some(type_name) => self.schema.type_by_name(type_name).expect("Type not found"),
None => self.current_type.clone(),
},
schema: self.schema,
context: self.context,
errors: RwLock::new(vec![]),
field_path: self.field_path.clone(),
}
}
#[doc(hidden)]
pub fn variables(&self) -> Variables<S> {
self.variables.clone()
}
#[doc(hidden)]
pub fn field_sub_executor(
&self,
field_alias: &'a str,
field_name: &'a str,
location: SourcePosition,
selection_set: Option<Vec<Selection<'a, S>>>,
) -> OwnedExecutor<'a, CtxT, S> {
OwnedExecutor {
fragments: self.fragments.clone(),
variables: self.variables.clone(),
current_selection_set: selection_set,
parent_selection_set: self.current_selection_set.clone(),
current_type: self.schema.make_type(
&self
.current_type
.innermost_concrete()
.field_by_name(field_name)
.expect("Field not found on inner type")
.field_type,
),
schema: self.schema,
context: self.context,
errors: RwLock::new(vec![]),
field_path: Arc::new(FieldPath::Field(
field_alias,
location,
Arc::clone(&self.field_path),
)),
}
}
#[doc(hidden)]
pub fn as_executor(&self) -> Executor<'_, '_, CtxT, S> {
Executor {
fragments: &self.fragments,
variables: &self.variables,
current_selection_set: if let Some(s) = &self.current_selection_set {
Some(&s[..])
} else {
None
},
parent_selection_set: if let Some(s) = &self.parent_selection_set {
Some(&s[..])
} else {
None
},
current_type: self.current_type.clone(),
schema: self.schema,
context: self.context,
errors: &self.errors,
field_path: Arc::clone(&self.field_path),
}
}
}
impl<'a, CtxT, S> OwnedExecutor<'a, CtxT, S> {
#[doc(hidden)]
pub fn fragment_by_name<'b>(&'b self, name: &str) -> Option<&'b Fragment<'a, S>> {
self.fragments.get(name)
}
#[doc(hidden)]
pub fn context(&self) -> &'a CtxT {
self.context
}
#[doc(hidden)]
pub fn schema(&self) -> &'a SchemaType<S> {
self.schema
}
#[doc(hidden)]
pub fn location(&self) -> &SourcePosition {
self.field_path.location()
}
}

View file

@ -77,7 +77,7 @@ impl Mutation {}
#[tokio::test]
async fn async_simple() {
let schema = RootNode::new(Query, Mutation);
let schema = RootNode::new(Query, Mutation, crate::EmptySubscription::new());
let doc = r#"
query {
fieldSync

View file

@ -1,7 +1,7 @@
use crate::{
executor::Variables,
schema::model::RootNode,
types::scalars::EmptyMutation,
types::scalars::{EmptyMutation, EmptySubscription},
value::{DefaultScalarValue, Object, Value},
};
@ -22,7 +22,11 @@ async fn run_variable_query<F>(query: &str, vars: Variables<DefaultScalarValue>,
where
F: Fn(&Object<DefaultScalarValue>) -> (),
{
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let (result, errs) = crate::execute(query, None, &schema, &vars, &())
.await

View file

@ -5,7 +5,7 @@ use crate::{
executor::Variables,
parser::SourcePosition,
schema::model::RootNode,
types::scalars::EmptyMutation,
types::scalars::{EmptyMutation, EmptySubscription},
validation::RuleError,
value::{DefaultScalarValue, Object, Value},
GraphQLError::ValidationError,
@ -34,7 +34,11 @@ async fn run_variable_query<F>(query: &str, vars: Variables<DefaultScalarValue>,
where
F: Fn(&Object<DefaultScalarValue>) -> (),
{
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let (result, errs) = crate::execute(query, None, &schema, &vars, &())
.await
@ -80,7 +84,11 @@ async fn serializes_as_output() {
#[tokio::test]
async fn does_not_accept_string_literals() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query = r#"{ toString(color: "RED") }"#;
let vars = vec![].into_iter().collect();
@ -117,7 +125,11 @@ async fn accepts_strings_in_variables() {
#[tokio::test]
async fn does_not_accept_incorrect_enum_name_in_variables() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query = r#"query q($color: Color!) { toString(color: $color) }"#;
let vars = vec![("color".to_owned(), InputValue::scalar("BLURPLE"))]
@ -139,7 +151,11 @@ async fn does_not_accept_incorrect_enum_name_in_variables() {
#[tokio::test]
async fn does_not_accept_incorrect_type_in_variables() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query = r#"query q($color: Color!) { toString(color: $color) }"#;
let vars = vec![("color".to_owned(), InputValue::scalar(123))]

View file

@ -1,6 +1,9 @@
mod field_execution {
use crate::{
ast::InputValue, schema::model::RootNode, types::scalars::EmptyMutation, value::Value,
ast::InputValue,
schema::model::RootNode,
types::scalars::{EmptyMutation, EmptySubscription},
value::Value,
};
struct DataType;
@ -55,8 +58,11 @@ mod field_execution {
#[tokio::test]
async fn test() {
let schema =
RootNode::<_, _, crate::DefaultScalarValue>::new(DataType, EmptyMutation::<()>::new());
let schema = RootNode::<_, _, _, crate::DefaultScalarValue>::new(
DataType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let doc = r"
query Example($size: Int) {
a,
@ -156,7 +162,11 @@ mod field_execution {
}
mod merge_parallel_fragments {
use crate::{schema::model::RootNode, types::scalars::EmptyMutation, value::Value};
use crate::{
schema::model::RootNode,
types::scalars::{EmptyMutation, EmptySubscription},
value::Value,
};
struct Type;
@ -178,7 +188,11 @@ mod merge_parallel_fragments {
#[tokio::test]
async fn test() {
let schema = RootNode::new(Type, EmptyMutation::<()>::new());
let schema = RootNode::new(
Type,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let doc = r"
{ a, ...FragOne, ...FragTwo }
fragment FragOne on Type {
@ -238,7 +252,11 @@ mod merge_parallel_fragments {
}
mod merge_parallel_inline_fragments {
use crate::{schema::model::RootNode, types::scalars::EmptyMutation, value::Value};
use crate::{
schema::model::RootNode,
types::scalars::{EmptyMutation, EmptySubscription},
value::Value,
};
struct Type;
struct Other;
@ -283,7 +301,11 @@ mod merge_parallel_inline_fragments {
#[tokio::test]
async fn test() {
let schema = RootNode::new(Type, EmptyMutation::<()>::new());
let schema = RootNode::new(
Type,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let doc = r"
{ a, ...FragOne }
fragment FragOne on Type {
@ -382,7 +404,10 @@ mod merge_parallel_inline_fragments {
mod threads_context_correctly {
use crate::{
executor::Context, schema::model::RootNode, types::scalars::EmptyMutation, value::Value,
executor::Context,
schema::model::RootNode,
types::scalars::{EmptyMutation, EmptySubscription},
value::Value,
};
struct Schema;
@ -404,7 +429,11 @@ mod threads_context_correctly {
#[tokio::test]
async fn test() {
let schema = RootNode::new(Schema, EmptyMutation::<TestContext>::new());
let schema = RootNode::new(
Schema,
EmptyMutation::<TestContext>::new(),
EmptySubscription::<TestContext>::new(),
);
let doc = r"{ a }";
let vars = vec![].into_iter().collect();
@ -443,7 +472,7 @@ mod dynamic_context_switching {
executor::{Context, ExecutionError, FieldError, FieldResult},
parser::SourcePosition,
schema::model::RootNode,
types::scalars::EmptyMutation,
types::scalars::{EmptyMutation, EmptySubscription},
value::Value,
};
@ -501,7 +530,11 @@ mod dynamic_context_switching {
#[tokio::test]
async fn test_opt() {
let schema = RootNode::new(Schema, EmptyMutation::<OuterContext>::new());
let schema = RootNode::new(
Schema,
EmptyMutation::<OuterContext>::new(),
EmptySubscription::<OuterContext>::new(),
);
let doc = r"{ first: itemOpt(key: 0) { value }, missing: itemOpt(key: 2) { value } }";
let vars = vec![].into_iter().collect();
@ -555,7 +588,11 @@ mod dynamic_context_switching {
#[tokio::test]
async fn test_res_success() {
let schema = RootNode::new(Schema, EmptyMutation::<OuterContext>::new());
let schema = RootNode::new(
Schema,
EmptyMutation::<OuterContext>::new(),
EmptySubscription::<OuterContext>::new(),
);
let doc = r"
{
first: itemRes(key: 0) { value }
@ -610,7 +647,11 @@ mod dynamic_context_switching {
#[tokio::test]
async fn test_res_fail() {
let schema = RootNode::new(Schema, EmptyMutation::<OuterContext>::new());
let schema = RootNode::new(
Schema,
EmptyMutation::<OuterContext>::new(),
EmptySubscription::<OuterContext>::new(),
);
let doc = r"
{
missing: itemRes(key: 2) { value }
@ -658,7 +699,11 @@ mod dynamic_context_switching {
#[tokio::test]
async fn test_res_opt() {
let schema = RootNode::new(Schema, EmptyMutation::<OuterContext>::new());
let schema = RootNode::new(
Schema,
EmptyMutation::<OuterContext>::new(),
EmptySubscription::<OuterContext>::new(),
);
let doc = r"
{
first: itemResOpt(key: 0) { value }
@ -726,7 +771,11 @@ mod dynamic_context_switching {
#[tokio::test]
async fn test_always() {
let schema = RootNode::new(Schema, EmptyMutation::<OuterContext>::new());
let schema = RootNode::new(
Schema,
EmptyMutation::<OuterContext>::new(),
EmptySubscription::<OuterContext>::new(),
);
let doc = r"{ first: itemAlways(key: 0) { value } }";
let vars = vec![].into_iter().collect();
@ -781,7 +830,7 @@ mod propagates_errors_to_nullable_fields {
executor::{ExecutionError, FieldError, FieldResult, IntoFieldError},
parser::SourcePosition,
schema::model::RootNode,
types::scalars::EmptyMutation,
types::scalars::{EmptyMutation, EmptySubscription},
value::{ScalarValue, Value},
};
@ -842,7 +891,11 @@ mod propagates_errors_to_nullable_fields {
#[tokio::test]
async fn nullable_first_level() {
let schema = RootNode::new(Schema, EmptyMutation::<()>::new());
let schema = RootNode::new(
Schema,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let doc = r"{ inner { nullableErrorField } }";
let vars = vec![].into_iter().collect();
@ -870,7 +923,11 @@ mod propagates_errors_to_nullable_fields {
#[tokio::test]
async fn non_nullable_first_level() {
let schema = RootNode::new(Schema, EmptyMutation::<()>::new());
let schema = RootNode::new(
Schema,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let doc = r"{ inner { nonNullableErrorField } }";
let vars = vec![].into_iter().collect();
@ -895,7 +952,11 @@ mod propagates_errors_to_nullable_fields {
#[tokio::test]
async fn custom_error_first_level() {
let schema = RootNode::new(Schema, EmptyMutation::<()>::new());
let schema = RootNode::new(
Schema,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let doc = r"{ inner { customErrorField } }";
let vars = vec![].into_iter().collect();
@ -920,7 +981,11 @@ mod propagates_errors_to_nullable_fields {
#[tokio::test]
async fn nullable_nested_level() {
let schema = RootNode::new(Schema, EmptyMutation::<()>::new());
let schema = RootNode::new(
Schema,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let doc = r"{ inner { nullableField { nonNullableErrorField } } }";
let vars = vec![].into_iter().collect();
@ -948,7 +1013,11 @@ mod propagates_errors_to_nullable_fields {
#[tokio::test]
async fn non_nullable_nested_level() {
let schema = RootNode::new(Schema, EmptyMutation::<()>::new());
let schema = RootNode::new(
Schema,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let doc = r"{ inner { nonNullableField { nonNullableErrorField } } }";
let vars = vec![].into_iter().collect();
@ -973,7 +1042,11 @@ mod propagates_errors_to_nullable_fields {
#[tokio::test]
async fn nullable_innermost() {
let schema = RootNode::new(Schema, EmptyMutation::<()>::new());
let schema = RootNode::new(
Schema,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let doc = r"{ inner { nonNullableField { nullableErrorField } } }";
let vars = vec![].into_iter().collect();
@ -1001,7 +1074,11 @@ mod propagates_errors_to_nullable_fields {
#[tokio::test]
async fn non_null_list() {
let schema = RootNode::new(Schema, EmptyMutation::<()>::new());
let schema = RootNode::new(
Schema,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let doc = r"{ inners { nonNullableErrorField } }";
let vars = vec![].into_iter().collect();
@ -1026,7 +1103,11 @@ mod propagates_errors_to_nullable_fields {
#[tokio::test]
async fn non_null_list_of_nullable() {
let schema = RootNode::new(Schema, EmptyMutation::<()>::new());
let schema = RootNode::new(
Schema,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let doc = r"{ nullableInners { nonNullableErrorField } }";
let vars = vec![].into_iter().collect();
@ -1077,7 +1158,10 @@ mod propagates_errors_to_nullable_fields {
mod named_operations {
use crate::{
schema::model::RootNode, types::scalars::EmptyMutation, value::Value, GraphQLError,
schema::model::RootNode,
types::scalars::{EmptyMutation, EmptySubscription},
value::Value,
GraphQLError,
};
struct Schema;
@ -1092,8 +1176,11 @@ mod named_operations {
#[tokio::test]
async fn uses_inline_operation_if_no_name_provided() {
let schema =
RootNode::<_, _, crate::DefaultScalarValue>::new(Schema, EmptyMutation::<()>::new());
let schema = RootNode::<_, _, _, crate::DefaultScalarValue>::new(
Schema,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let doc = r"{ a }";
let vars = vec![].into_iter().collect();
@ -1112,7 +1199,11 @@ mod named_operations {
#[tokio::test]
async fn uses_only_named_operation() {
let schema = RootNode::new(Schema, EmptyMutation::<()>::new());
let schema = RootNode::new(
Schema,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let doc = r"query Example { a }";
let vars = vec![].into_iter().collect();
@ -1131,7 +1222,11 @@ mod named_operations {
#[tokio::test]
async fn uses_named_operation_if_name_provided() {
let schema = RootNode::new(Schema, EmptyMutation::<()>::new());
let schema = RootNode::new(
Schema,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let doc =
r"query Example($p: String!) { first: a(p: $p) } query OtherExample { second: a }";
@ -1151,7 +1246,11 @@ mod named_operations {
#[tokio::test]
async fn error_if_multiple_operations_provided_but_no_name() {
let schema = RootNode::new(Schema, EmptyMutation::<()>::new());
let schema = RootNode::new(
Schema,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let doc = r"query Example { first: a } query OtherExample { second: a }";
let vars = vec![].into_iter().collect();
@ -1165,7 +1264,11 @@ mod named_operations {
#[tokio::test]
async fn error_if_unknown_operation_name_provided() {
let schema = RootNode::new(Schema, EmptyMutation::<()>::new());
let schema = RootNode::new(
Schema,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let doc = r"query Example { first: a } query OtherExample { second: a }";
let vars = vec![].into_iter().collect();

View file

@ -1,5 +1,9 @@
mod interface {
use crate::{schema::model::RootNode, types::scalars::EmptyMutation, value::Value};
use crate::{
schema::model::RootNode,
types::scalars::{EmptyMutation, EmptySubscription},
value::Value,
};
trait Pet {
fn name(&self) -> &str;
@ -100,6 +104,7 @@ mod interface {
],
},
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let doc = r"
{
@ -156,7 +161,11 @@ mod interface {
}
mod union {
use crate::{schema::model::RootNode, types::scalars::EmptyMutation, value::Value};
use crate::{
schema::model::RootNode,
types::scalars::{EmptyMutation, EmptySubscription},
value::Value,
};
trait Pet {
fn as_dog(&self) -> Option<&Dog> {
@ -246,6 +255,7 @@ mod union {
],
},
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let doc = r"
{

View file

@ -3,7 +3,7 @@ use juniper_codegen::GraphQLEnumInternal as GraphQLEnum;
use crate::{
executor::Variables,
schema::model::RootNode,
types::scalars::EmptyMutation,
types::scalars::{EmptyMutation, EmptySubscription},
value::{DefaultScalarValue, Object, Value},
};
@ -92,7 +92,11 @@ async fn run_type_info_query<F>(doc: &str, f: F)
where
F: Fn((&Object<DefaultScalarValue>, &Vec<Value<DefaultScalarValue>>)) -> (),
{
let schema = RootNode::new(Root {}, EmptyMutation::<()>::new());
let schema = RootNode::new(
Root {},
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let (result, errs) = crate::execute(doc, None, &schema, &Variables::new(), &())
.await

View file

@ -6,7 +6,7 @@ use crate::{
ast::{FromInputValue, InputValue},
executor::Variables,
schema::model::RootNode,
types::scalars::EmptyMutation,
types::scalars::{EmptyMutation, EmptySubscription},
value::{DefaultScalarValue, Object, Value},
};
@ -117,7 +117,11 @@ async fn run_type_info_query<F>(doc: &str, f: F)
where
F: Fn(&Object<DefaultScalarValue>, &Vec<Value<DefaultScalarValue>>) -> (),
{
let schema = RootNode::new(Root {}, EmptyMutation::<()>::new());
let schema = RootNode::new(
Root {},
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let (result, errs) = crate::execute(doc, None, &schema, &Variables::new(), &())
.await

View file

@ -10,7 +10,7 @@ use self::input_object::{NamedPublic, NamedPublicWithDescription};
use crate::{
executor::Variables,
schema::model::RootNode,
types::scalars::EmptyMutation,
types::scalars::{EmptyMutation, EmptySubscription},
value::{ParseScalarResult, ParseScalarValue, Value},
};
@ -83,7 +83,11 @@ async fn test_execution() {
second: sampleScalar(first: 10 second: 20)
}
"#;
let schema = RootNode::new(Root, EmptyMutation::<()>::new());
let schema = RootNode::new(
Root,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let (result, errs) = crate::execute(doc, None, &schema, &Variables::new(), &())
.await
@ -128,7 +132,11 @@ async fn enum_introspection() {
}
}
"#;
let schema = RootNode::new(Root, EmptyMutation::<()>::new());
let schema = RootNode::new(
Root,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let (result, errs) = crate::execute(doc, None, &schema, &Variables::new(), &())
.await
@ -238,7 +246,11 @@ async fn interface_introspection() {
}
}
"#;
let schema = RootNode::new(Root, EmptyMutation::<()>::new());
let schema = RootNode::new(
Root,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let (result, errs) = crate::execute(doc, None, &schema, &Variables::new(), &())
.await
@ -386,7 +398,11 @@ async fn object_introspection() {
}
}
"#;
let schema = RootNode::new(Root, EmptyMutation::<()>::new());
let schema = RootNode::new(
Root,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let (result, errs) = crate::execute(doc, None, &schema, &Variables::new(), &())
.await
@ -594,7 +610,11 @@ async fn scalar_introspection() {
}
}
"#;
let schema = RootNode::new(Root, EmptyMutation::<()>::new());
let schema = RootNode::new(
Root,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let (result, errs) = crate::execute(doc, None, &schema, &Variables::new(), &())
.await

View file

@ -5,7 +5,7 @@ use crate::{
executor::Variables,
parser::SourcePosition,
schema::model::RootNode,
types::scalars::EmptyMutation,
types::scalars::{EmptyMutation, EmptySubscription},
validation::RuleError,
value::{DefaultScalarValue, Object, ParseScalarResult, ParseScalarValue, Value},
GraphQLError::ValidationError,
@ -130,7 +130,11 @@ async fn run_variable_query<F>(query: &str, vars: Variables<DefaultScalarValue>,
where
F: Fn(&Object<DefaultScalarValue>) -> (),
{
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let (result, errs) = crate::execute(query, None, &schema, &vars, &())
.await
@ -285,7 +289,11 @@ async fn variable_runs_from_input_value_on_scalar() {
#[tokio::test]
async fn variable_error_on_nested_non_null() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query = r#"query q($input: TestInputObject) { fieldWithObjectInput(input: $input) }"#;
let vars = vec![(
@ -318,7 +326,11 @@ async fn variable_error_on_nested_non_null() {
#[tokio::test]
async fn variable_error_on_incorrect_type() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query = r#"query q($input: TestInputObject) { fieldWithObjectInput(input: $input) }"#;
let vars = vec![("input".to_owned(), InputValue::scalar("foo bar"))]
@ -340,7 +352,11 @@ async fn variable_error_on_incorrect_type() {
#[tokio::test]
async fn variable_error_on_omit_non_null() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query = r#"query q($input: TestInputObject) { fieldWithObjectInput(input: $input) }"#;
let vars = vec![(
@ -372,7 +388,11 @@ async fn variable_error_on_omit_non_null() {
#[tokio::test]
async fn variable_multiple_errors_with_nesting() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query =
r#"query q($input: TestNestedInputObject) { fieldWithNestedObjectInput(input: $input) }"#;
@ -411,7 +431,11 @@ async fn variable_multiple_errors_with_nesting() {
#[tokio::test]
async fn variable_error_on_additional_field() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query = r#"query q($input: TestInputObject) { fieldWithObjectInput(input: $input) }"#;
let vars = vec![(
@ -535,7 +559,11 @@ async fn allow_nullable_inputs_to_be_set_to_value_directly() {
#[tokio::test]
async fn does_not_allow_non_nullable_input_to_be_omitted_in_variable() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query = r#"query q($value: String!) { fieldWithNonNullableStringInput(input: $value) }"#;
let vars = vec![].into_iter().collect();
@ -555,7 +583,11 @@ async fn does_not_allow_non_nullable_input_to_be_omitted_in_variable() {
#[tokio::test]
async fn does_not_allow_non_nullable_input_to_be_set_to_null_in_variable() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query = r#"query q($value: String!) { fieldWithNonNullableStringInput(input: $value) }"#;
let vars = vec![("value".to_owned(), InputValue::null())]
@ -669,7 +701,11 @@ async fn allow_lists_to_contain_null() {
#[tokio::test]
async fn does_not_allow_non_null_lists_to_be_null() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query = r#"query q($input: [String]!) { nnList(input: $input) }"#;
let vars = vec![("input".to_owned(), InputValue::null())]
@ -771,7 +807,11 @@ async fn allow_lists_of_non_null_to_contain_values() {
#[tokio::test]
async fn does_not_allow_lists_of_non_null_to_contain_null() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query = r#"query q($input: [String!]) { listNn(input: $input) }"#;
let vars = vec![(
@ -800,7 +840,11 @@ async fn does_not_allow_lists_of_non_null_to_contain_null() {
#[tokio::test]
async fn does_not_allow_non_null_lists_of_non_null_to_contain_null() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query = r#"query q($input: [String!]!) { nnListNn(input: $input) }"#;
let vars = vec![(
@ -829,7 +873,11 @@ async fn does_not_allow_non_null_lists_of_non_null_to_contain_null() {
#[tokio::test]
async fn does_not_allow_non_null_lists_of_non_null_to_be_null() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query = r#"query q($input: [String!]!) { nnListNn(input: $input) }"#;
let vars = vec![("value".to_owned(), InputValue::null())]
@ -975,7 +1023,11 @@ async fn nullable_input_object_arguments_successful_with_variables() {
#[tokio::test]
async fn does_not_allow_missing_required_field() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query = r#"{ exampleInput(arg: {a: "abc"}) }"#;
let vars = vec![].into_iter().collect();
@ -995,7 +1047,11 @@ async fn does_not_allow_missing_required_field() {
#[tokio::test]
async fn does_not_allow_null_in_required_field() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query = r#"{ exampleInput(arg: {a: "abc", b: null}) }"#;
let vars = vec![].into_iter().collect();
@ -1015,7 +1071,11 @@ async fn does_not_allow_null_in_required_field() {
#[tokio::test]
async fn does_not_allow_missing_variable_for_required_field() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query = r#"query q($var: Int!) { exampleInput(arg: {b: $var}) }"#;
let vars = vec![].into_iter().collect();
@ -1035,7 +1095,11 @@ async fn does_not_allow_missing_variable_for_required_field() {
#[tokio::test]
async fn does_not_allow_null_variable_for_required_field() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query = r#"query q($var: Int!) { exampleInput(arg: {b: $var}) }"#;
let vars = vec![("var".to_owned(), InputValue::null())]
@ -1142,7 +1206,11 @@ mod integers {
#[tokio::test]
async fn does_not_coerce_from_float() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query = r#"query q($var: Int!) { integerInput(value: $var) }"#;
let vars = vec![("var".to_owned(), InputValue::scalar(10.0))]
@ -1164,7 +1232,11 @@ mod integers {
#[tokio::test]
async fn does_not_coerce_from_string() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query = r#"query q($var: Int!) { integerInput(value: $var) }"#;
let vars = vec![("var".to_owned(), InputValue::scalar("10"))]
@ -1224,7 +1296,11 @@ mod floats {
#[tokio::test]
async fn does_not_coerce_from_string() {
let schema = RootNode::new(TestType, EmptyMutation::<()>::new());
let schema = RootNode::new(
TestType,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let query = r#"query q($var: Float!) { floatInput(value: $var) }"#;
let vars = vec![("var".to_owned(), InputValue::scalar("10"))]

View file

@ -11,9 +11,10 @@ use serde_derive::{Deserialize, Serialize};
use crate::{
ast::InputValue,
executor::ExecutionError,
executor::{ExecutionError, ValuesStream},
value::{DefaultScalarValue, ScalarValue},
FieldError, GraphQLError, GraphQLType, RootNode, Value, Variables,
FieldError, GraphQLError, GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, RootNode,
Value, Variables,
};
/// The expected structure of the decoded JSON document for either POST or GET requests.
@ -70,19 +71,20 @@ where
}
}
/// Execute a GraphQL request using the specified schema and context
/// Execute a GraphQL request synchronously using the specified schema and context
///
/// This is a simple wrapper around the `execute` function exposed at the
/// This is a simple wrapper around the `execute_sync` function exposed at the
/// top level of this crate.
pub fn execute_sync<'a, CtxT, QueryT, MutationT>(
pub fn execute_sync<'a, CtxT, QueryT, MutationT, SubscriptionT>(
&'a self,
root_node: &'a RootNode<QueryT, MutationT, S>,
root_node: &'a RootNode<QueryT, MutationT, SubscriptionT, S>,
context: &CtxT,
) -> GraphQLResponse<'a, S>
where
S: ScalarValue,
QueryT: GraphQLType<S, Context = CtxT>,
MutationT: GraphQLType<S, Context = CtxT>,
SubscriptionT: GraphQLType<S, Context = CtxT>,
{
GraphQLResponse(crate::execute_sync(
&self.query,
@ -97,9 +99,9 @@ where
///
/// This is a simple wrapper around the `execute` function exposed at the
/// top level of this crate.
pub async fn execute<'a, CtxT, QueryT, MutationT>(
pub async fn execute<'a, CtxT, QueryT, MutationT, SubscriptionT>(
&'a self,
root_node: &'a RootNode<'a, QueryT, MutationT, S>,
root_node: &'a RootNode<'a, QueryT, MutationT, SubscriptionT, S>,
context: &'a CtxT,
) -> GraphQLResponse<'a, S>
where
@ -108,6 +110,8 @@ where
QueryT::TypeInfo: Send + Sync,
MutationT: crate::GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
MutationT::TypeInfo: Send + Sync,
SubscriptionT: GraphQLType<S, Context = CtxT> + Send + Sync,
SubscriptionT::TypeInfo: Send + Sync,
CtxT: Send + Sync,
{
let op = self.operation_name();
@ -117,6 +121,34 @@ where
}
}
/// Resolve a GraphQL subscription into `Value<ValuesStream<S>` using the
/// specified schema and context.
/// This is a wrapper around the `resolve_into_stream` function exposed at the top
/// level of this crate.
pub async fn resolve_into_stream<'req, 'rn, 'ctx, 'a, CtxT, QueryT, MutationT, SubscriptionT, S>(
req: &'req GraphQLRequest<S>,
root_node: &'rn RootNode<'a, QueryT, MutationT, SubscriptionT, S>,
context: &'ctx CtxT,
) -> Result<(Value<ValuesStream<'a, S>>, Vec<ExecutionError<S>>), GraphQLError<'a>>
where
'req: 'a,
'rn: 'a,
'ctx: 'a,
S: ScalarValue + Send + Sync + 'static,
QueryT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
QueryT::TypeInfo: Send + Sync,
MutationT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
MutationT::TypeInfo: Send + Sync,
SubscriptionT: GraphQLSubscriptionType<S, Context = CtxT> + Send + Sync,
SubscriptionT::TypeInfo: Send + Sync,
CtxT: Send + Sync,
{
let op = req.operation_name();
let vars = req.variables();
crate::resolve_into_stream(&req.query, op, root_node, &vars, context).await
}
/// Simple wrapper around the result from executing a GraphQL query
///
/// This struct implements Serialize, so you can simply serialize this
@ -130,6 +162,11 @@ impl<'a, S> GraphQLResponse<'a, S>
where
S: ScalarValue,
{
/// Constructs new `GraphQLResponse` using the given result
pub fn from_result(r: Result<(Value<S>, Vec<ExecutionError<S>>), GraphQLError<'a>>) -> Self {
Self(r)
}
/// Constructs an error response outside of the normal execution flow
pub fn error(error: FieldError<S>) -> Self {
GraphQLResponse(Ok((Value::null(), vec![ExecutionError::at_origin(error)])))

View file

@ -2,7 +2,16 @@
/// Generate the HTML source to show a GraphQL Playground interface
// source: https://github.com/prisma/graphql-playground/blob/master/packages/graphql-playground-html/withAnimation.html
pub fn playground_source(graphql_endpoint_url: &str) -> String {
pub fn playground_source(
graphql_endpoint_url: &str,
subscriptions_endpoint_url: Option<&str>,
) -> String {
let subscriptions_endpoint = if let Some(sub_url) = subscriptions_endpoint_url {
sub_url
} else {
graphql_endpoint_url
};
r##"
<!DOCTYPE html>
@ -14,7 +23,7 @@ pub fn playground_source(graphql_endpoint_url: &str) -> String {
<title>GraphQL Playground</title>
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/graphql-playground-react/build/static/css/index.css" />
<link rel="shortcut icon" href="//cdn.jsdelivr.net/npm/graphql-playground-react/build/favicon.png" />
<script src="//cdn.jsdelivr.net/npm/graphql-playground-react/build/static/js/middleware.js"></script>
<script src="//cdn.jsdelivr.net/npm/graphql-playground-react@1.7.20/build/static/js/middleware.js"></script>
</head>
@ -537,10 +546,11 @@ pub fn playground_source(graphql_endpoint_url: &str) -> String {
const root = document.getElementById('root');
root.classList.add('playgroundIn');
GraphQLPlayground.init(root, { endpoint: 'JUNIPER_GRAPHQL_URL' })
GraphQLPlayground.init(root, { endpoint: 'JUNIPER_GRAPHQL_URL', subscriptionEndpoint: 'JUNIPER_SUBSCRIPTIONS_URL' })
})
</script>
</body>
</html>
"##.replace("JUNIPER_GRAPHQL_URL", graphql_endpoint_url)
.replace("JUNIPER_SUBSCRIPTIONS_URL", subscriptions_endpoint)
}

View file

@ -203,7 +203,10 @@ mod integration_test {
use chrono::{prelude::*, Utc};
use crate::{
executor::Variables, schema::model::RootNode, types::scalars::EmptyMutation, value::Value,
executor::Variables,
schema::model::RootNode,
types::scalars::{EmptyMutation, EmptySubscription},
value::Value,
};
#[tokio::test]
@ -235,7 +238,11 @@ mod integration_test {
}
"#;
let schema = RootNode::new(Root, EmptyMutation::<()>::new());
let schema = RootNode::new(
Root,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let (result, errs) = crate::execute(doc, None, &schema, &Variables::new(), &())
.await

View file

@ -74,6 +74,10 @@ impl<'a> ser::Serialize for GraphQLError<'a> {
message: "Expected query, got subscription",
}]
.serialize(serializer),
GraphQLError::NotSubscription => [SerializeHelper {
message: "Expected subscription, got query",
}]
.serialize(serializer),
}
}
}

View file

@ -115,15 +115,15 @@ extern crate bson;
// This allows users to just depend on juniper and get the derive
// functionality automatically.
pub use juniper_codegen::{
graphql_object, graphql_union, GraphQLEnum, GraphQLInputObject, GraphQLObject,
GraphQLScalarValue,
graphql_object, graphql_subscription, graphql_union, GraphQLEnum, GraphQLInputObject,
GraphQLObject, GraphQLScalarValue,
};
// Internal macros are not exported,
// but declared at the root to make them easier to use.
#[allow(unused_imports)]
use juniper_codegen::{
graphql_object_internal, graphql_union_internal, GraphQLEnumInternal,
GraphQLInputObjectInternal, GraphQLScalarValueInternal,
graphql_object_internal, graphql_subscription_internal, graphql_union_internal,
GraphQLEnumInternal, GraphQLInputObjectInternal, GraphQLScalarValueInternal,
};
#[macro_use]
@ -169,16 +169,19 @@ pub use crate::{
executor::{
Applies, Context, ExecutionError, ExecutionResult, Executor, FieldError, FieldResult,
FromContext, IntoFieldError, IntoResolvable, LookAheadArgument, LookAheadMethods,
LookAheadSelection, LookAheadValue, Registry, Variables,
LookAheadSelection, LookAheadValue, OwnedExecutor, Registry, ValuesStream, Variables,
},
introspection::IntrospectionFormat,
macros::subscription_helpers::{ExtractTypeFromStream, IntoFieldResult},
schema::{
meta,
model::{RootNode, SchemaType},
},
types::{
async_await::GraphQLTypeAsync,
base::{Arguments, GraphQLType, TypeKind},
scalars::{EmptyMutation, ID},
scalars::{EmptyMutation, EmptySubscription, ID},
subscriptions::{GraphQLSubscriptionType, SubscriptionConnection, SubscriptionCoordinator},
},
validation::RuleError,
value::{DefaultScalarValue, Object, ParseScalarResult, ParseScalarValue, ScalarValue, Value},
@ -187,8 +190,6 @@ pub use crate::{
/// A pinned, boxed future that can be polled.
pub type BoxFuture<'a, T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + 'a + Send>>;
pub use crate::types::async_await::GraphQLTypeAsync;
/// An error that prevented query execution
#[derive(Debug, PartialEq)]
#[allow(missing_docs)]
@ -199,6 +200,7 @@ pub enum GraphQLError<'a> {
MultipleOperationsProvided,
UnknownOperationName,
IsSubscription,
NotSubscription,
}
impl<'a> fmt::Display for GraphQLError<'a> {
@ -214,18 +216,19 @@ impl<'a> fmt::Display for GraphQLError<'a> {
GraphQLError::NoOperationProvided => write!(f, "No operation provided"),
GraphQLError::MultipleOperationsProvided => write!(f, "Multiple operations provided"),
GraphQLError::UnknownOperationName => write!(f, "Unknown operation name"),
GraphQLError::IsSubscription => write!(f, "Subscription are not currently supported"),
GraphQLError::IsSubscription => write!(f, "Operation is a subscription"),
GraphQLError::NotSubscription => write!(f, "Operation is not a subscription"),
}
}
}
impl<'a> std::error::Error for GraphQLError<'a> {}
/// Execute a query in a provided schema
pub fn execute_sync<'a, S, CtxT, QueryT, MutationT>(
/// Execute a query synchronously in a provided schema
pub fn execute_sync<'a, S, CtxT, QueryT, MutationT, SubscriptionT>(
document_source: &'a str,
operation_name: Option<&str>,
root_node: &'a RootNode<QueryT, MutationT, S>,
root_node: &'a RootNode<QueryT, MutationT, SubscriptionT, S>,
variables: &Variables<S>,
context: &CtxT,
) -> Result<(Value<S>, Vec<ExecutionError<S>>), GraphQLError<'a>>
@ -233,6 +236,7 @@ where
S: ScalarValue,
QueryT: GraphQLType<S, Context = CtxT>,
MutationT: GraphQLType<S, Context = CtxT>,
SubscriptionT: GraphQLType<S, Context = CtxT>,
{
let document = parse_document_source(document_source, &root_node.schema)?;
@ -260,10 +264,10 @@ where
}
/// Execute a query in a provided schema
pub async fn execute<'a, S, CtxT, QueryT, MutationT>(
pub async fn execute<'a, S, CtxT, QueryT, MutationT, SubscriptionT>(
document_source: &'a str,
operation_name: Option<&str>,
root_node: &'a RootNode<'a, QueryT, MutationT, S>,
root_node: &'a RootNode<'a, QueryT, MutationT, SubscriptionT, S>,
variables: &Variables<S>,
context: &CtxT,
) -> Result<(Value<S>, Vec<ExecutionError<S>>), GraphQLError<'a>>
@ -273,6 +277,8 @@ where
QueryT::TypeInfo: Send + Sync,
MutationT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
MutationT::TypeInfo: Send + Sync,
SubscriptionT: GraphQLType<S, Context = CtxT> + Send + Sync,
SubscriptionT::TypeInfo: Send + Sync,
CtxT: Send + Sync,
{
let document = parse_document_source(document_source, &root_node.schema)?;
@ -301,9 +307,44 @@ where
.await
}
/// Resolve subscription into `ValuesStream`
pub async fn resolve_into_stream<'a, S, CtxT, QueryT, MutationT, SubscriptionT>(
document_source: &'a str,
operation_name: Option<&str>,
root_node: &'a RootNode<'a, QueryT, MutationT, SubscriptionT, S>,
variables: &Variables<S>,
context: &'a CtxT,
) -> Result<(Value<ValuesStream<'a, S>>, Vec<ExecutionError<S>>), GraphQLError<'a>>
where
S: ScalarValue + Send + Sync,
QueryT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
QueryT::TypeInfo: Send + Sync,
MutationT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
MutationT::TypeInfo: Send + Sync,
SubscriptionT: GraphQLSubscriptionType<S, Context = CtxT> + Send + Sync,
SubscriptionT::TypeInfo: Send + Sync,
CtxT: Send + Sync,
{
let document: crate::ast::Document<'a, S> =
parse_document_source(document_source, &root_node.schema)?;
let operation = get_operation(&document, operation_name)?;
{
let errors = validate_input_values(&variables, operation, &root_node.schema);
if !errors.is_empty() {
return Err(GraphQLError::ValidationError(errors));
}
}
executor::resolve_validated_subscription(&document, operation, root_node, variables, context)
.await
}
/// Execute the reference introspection query in the provided schema
pub fn introspect<'a, S, CtxT, QueryT, MutationT>(
root_node: &'a RootNode<QueryT, MutationT, S>,
pub fn introspect<'a, S, CtxT, QueryT, MutationT, SubscriptionT>(
root_node: &'a RootNode<QueryT, MutationT, SubscriptionT, S>,
context: &CtxT,
format: IntrospectionFormat,
) -> Result<(Value<S>, Vec<ExecutionError<S>>), GraphQLError<'a>>
@ -311,6 +352,7 @@ where
S: ScalarValue,
QueryT: GraphQLType<S, Context = CtxT>,
MutationT: GraphQLType<S, Context = CtxT>,
SubscriptionT: GraphQLType<S, Context = CtxT>,
{
execute_sync(
match format {

View file

@ -75,7 +75,7 @@ impl Droid {
}
// You can introduce lifetimes or generic parameters by < > before the name.
juniper::graphql_interface!(<'a> &'a Character: Database as "Character" |&self| {
juniper::graphql_interface!(<'a> &'a dyn Character: Database as "Character" |&self| {
field id() -> &str { self.id() }
instance_resolvers: |&context| {

View file

@ -1,4 +1,5 @@
// Wrapper macros which allows built-in macros to be recognized as "crate-local".
// Wrapper macros which allows built-in macros to be recognized as "crate-local"
// and helper traits for #[juniper::graphql_subscription] macro.
#[macro_use]
mod common;
@ -9,3 +10,5 @@ mod scalar;
#[cfg(test)]
mod tests;
pub mod subscription_helpers;

View file

@ -0,0 +1,99 @@
//! Helper types for converting types to `Result<T, FieldError<E>>`.
//!
//! Used in `#[graphql_subscription]` macros to convert result type aliases on
//! subscription handlers to a concrete return type.
use futures::Stream;
use crate::{FieldError, GraphQLType, ScalarValue};
/// Trait for converting `T` to `Ok(T)` if T is not Result.
/// This is useful in subscription macros when user can provide type alias for
/// Stream or Result<Stream, _> and then a function on Stream should be called.
pub trait IntoFieldResult<T, S> {
/// Turn current type into a generic result
fn into_result(self) -> Result<T, FieldError<S>>;
}
impl<T, E, S> IntoFieldResult<T, S> for Result<T, E>
where
E: Into<FieldError<S>>,
{
fn into_result(self) -> Result<T, FieldError<S>> {
self.map_err(|e| e.into())
}
}
impl<T, I, S> IntoFieldResult<T, S> for T
where
T: Stream<Item = I>,
{
fn into_result(self) -> Result<T, FieldError<S>> {
Ok(self)
}
}
/// This struct is used in `ExtractTypeFromStream` implementation for streams
/// of values.
pub struct StreamItem;
/// This struct is used in `ExtractTypeFromStream` implementation for results
/// with streams of values inside.
pub struct StreamResult;
/// This struct is used in `ExtractTypeFromStream` implementation for streams
/// of results of values inside.
pub struct ResultStreamItem;
/// This struct is used in `ExtractTypeFromStream` implementation for results
/// with streams of results of values inside.
pub struct ResultStreamResult;
/// This trait is used in `juniper::graphql_subscription` macro to get stream's
/// item type that implements `GraphQLType` from type alias provided
/// by user.
pub trait ExtractTypeFromStream<T, S>
where
S: ScalarValue,
{
/// Stream's return Value that will be returned if
/// no errors occured. Is used to determine field type in
/// `#[juniper::graphql_subscription]`
type Item: GraphQLType<S>;
}
impl<T, I, S> ExtractTypeFromStream<StreamItem, S> for T
where
T: futures::Stream<Item = I>,
I: GraphQLType<S>,
S: ScalarValue,
{
type Item = I;
}
impl<Ty, T, E, S> ExtractTypeFromStream<StreamResult, S> for Ty
where
Ty: futures::Stream<Item = Result<T, E>>,
T: GraphQLType<S>,
S: ScalarValue,
{
type Item = T;
}
impl<T, I, E, S> ExtractTypeFromStream<ResultStreamItem, S> for Result<T, E>
where
T: futures::Stream<Item = I>,
I: GraphQLType<S>,
S: ScalarValue,
{
type Item = I;
}
impl<T, E, I, ER, S> ExtractTypeFromStream<ResultStreamResult, S> for Result<T, E>
where
T: futures::Stream<Item = Result<I, ER>>,
I: GraphQLType<S>,
S: ScalarValue,
{
type Item = I;
}

View file

@ -3,7 +3,7 @@ use juniper_codegen::GraphQLInputObjectInternal as GraphQLInputObject;
use crate::{
executor::Variables,
schema::model::RootNode,
types::scalars::EmptyMutation,
types::scalars::{EmptyMutation, EmptySubscription},
value::{DefaultScalarValue, Value},
};
@ -75,15 +75,15 @@ impl Root {
// TODO: enable once [parameter attributes are supported by proc macros]
// (https://github.com/graphql-rust/juniper/pull/441)
// fn attr_arg_descr(
// #[graphql(description = "The arg")]
// arg: i32) -> i32
// { 0 }
// fn attr_arg_descr_collapse(
// #[graphql(description = "The first arg")]
// #[graphql(description = "and more details")]
// arg: i32,
// ) -> i32 { 0 }
//fn attr_arg_descr(
// #[graphql(description = "The arg")]
// arg: i32) -> i32
//{ 0 }
//fn attr_arg_descr_collapse(
// #[graphql(description = "The first arg")]
// #[graphql(description = "and more details")]
// arg: i32,
//) -> i32 { 0 }
#[graphql(arguments(arg(default = 123,),))]
fn arg_with_default(arg: i32) -> i32 {
@ -164,7 +164,11 @@ where
}
}
"#;
let schema = RootNode::new(Root {}, EmptyMutation::<()>::new());
let schema = RootNode::new(
Root {},
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let (result, errs) = crate::execute(doc, None, &schema, &Variables::new(), &())
.await

View file

@ -2,7 +2,7 @@ use crate::{
ast::InputValue,
executor::FieldResult,
schema::model::RootNode,
types::scalars::EmptyMutation,
types::scalars::{EmptyMutation, EmptySubscription},
value::{DefaultScalarValue, Object, Value},
};
@ -159,7 +159,11 @@ where
}
}
"#;
let schema = RootNode::new(Root {}, EmptyMutation::<()>::new());
let schema = RootNode::new(
Root {},
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let vars = vec![("typeName".to_owned(), InputValue::scalar(type_name))]
.into_iter()
.collect();

View file

@ -1,5 +1,5 @@
use super::util;
use crate::{graphql_value, EmptyMutation, RootNode};
use crate::{graphql_value, EmptyMutation, EmptySubscription, RootNode};
#[derive(Default)]
struct Context {
@ -122,9 +122,19 @@ impl Mutation {
}
}
#[derive(Default)]
struct Subscription;
#[crate::graphql_object_internal(context = Context)]
impl Subscription {
fn empty() -> bool {
true
}
}
#[tokio::test]
async fn object_introspect() {
let res = util::run_info_query::<Query, Mutation, Context>("Query").await;
let res = util::run_info_query::<Query, Mutation, Subscription, Context>("Query").await;
assert_eq!(
res,
crate::graphql_value!({
@ -266,7 +276,11 @@ async fn object_query() {
withMutArg(arg: true)
}
"#;
let schema = RootNode::new(Query { b: true }, EmptyMutation::<Context>::new());
let schema = RootNode::new(
Query { b: true },
EmptyMutation::<Context>::new(),
EmptySubscription::<Context>::new(),
);
let vars = std::collections::HashMap::new();
let (result, errs) = crate::execute(doc, None, &schema, &vars, &Context { flag1: true })

View file

@ -0,0 +1,355 @@
use std::pin::Pin;
use futures::StreamExt as _;
use crate::{graphql_value, EmptyMutation, RootNode, Value};
use super::util;
#[derive(Default)]
struct Context {
flag1: bool,
}
impl crate::Context for Context {}
struct WithLifetime<'a> {
value: &'a str,
}
#[crate::graphql_object_internal(Context=Context)]
impl<'a> WithLifetime<'a> {
fn value(&'a self) -> &'a str {
self.value
}
}
struct WithContext;
#[crate::graphql_object_internal(Context=Context)]
impl WithContext {
fn ctx(ctx: &Context) -> bool {
ctx.flag1
}
}
#[derive(Default)]
struct Query;
#[crate::graphql_object_internal(
Context = Context,
)]
impl Query {
fn empty() -> bool {
true
}
}
#[derive(Default)]
struct Mutation;
#[crate::graphql_object_internal(context = Context)]
impl Mutation {
fn empty() -> bool {
true
}
}
type Stream<I> = Pin<Box<dyn futures::Stream<Item = I> + Send>>;
#[derive(Default)]
struct Subscription {
b: bool,
}
#[crate::graphql_subscription_internal(
scalar = crate::DefaultScalarValue,
name = "Subscription",
context = Context,
)]
/// Subscription Description.
impl Subscription {
#[graphql(description = "With Self Description")]
async fn with_self(&self) -> Stream<bool> {
let b = self.b;
Box::pin(futures::stream::once(async move { b }))
}
async fn independent() -> Stream<i32> {
Box::pin(futures::stream::once(async { 100 }))
}
async fn with_executor(_exec: &Executor<Context>) -> Stream<bool> {
Box::pin(futures::stream::once(async { true }))
}
async fn with_executor_and_self(&self, _exec: &Executor<Context>) -> Stream<bool> {
Box::pin(futures::stream::once(async { true }))
}
async fn with_context(_context: &Context) -> Stream<bool> {
Box::pin(futures::stream::once(async { true }))
}
async fn with_context_and_self(&self, _context: &Context) -> Stream<bool> {
Box::pin(futures::stream::once(async { true }))
}
#[graphql(name = "renamed")]
async fn has_custom_name() -> Stream<bool> {
Box::pin(futures::stream::once(async { true }))
}
#[graphql(description = "attr")]
async fn has_description_attr() -> Stream<bool> {
Box::pin(futures::stream::once(async { true }))
}
/// Doc description
async fn has_description_doc_comment() -> Stream<bool> {
Box::pin(futures::stream::once(async { true }))
}
async fn has_argument(arg1: bool) -> Stream<bool> {
Box::pin(futures::stream::once(async move { arg1 }))
}
#[graphql(arguments(default_arg(default = true)))]
async fn default_argument(default_arg: bool) -> Stream<bool> {
Box::pin(futures::stream::once(async move { default_arg }))
}
#[graphql(arguments(arg(description = "my argument description")))]
async fn arg_with_description(arg: bool) -> Stream<bool> {
Box::pin(futures::stream::once(async move { arg }))
}
async fn with_context_child(&self) -> Stream<WithContext> {
Box::pin(futures::stream::once(async { WithContext }))
}
async fn with_implicit_lifetime_child(&self) -> Stream<WithLifetime<'_>> {
Box::pin(futures::stream::once(async {
WithLifetime { value: "blub" }
}))
}
async fn with_mut_arg(mut arg: bool) -> Stream<bool> {
if arg {
arg = !arg;
}
Box::pin(futures::stream::once(async move { arg }))
}
async fn without_type_alias() -> Pin<Box<dyn futures::Stream<Item = &str> + Send>> {
Box::pin(futures::stream::once(async { "abc" }))
}
}
#[tokio::test]
async fn object_introspect() {
let res = util::run_info_query::<Query, Mutation, Subscription, Context>("Subscription").await;
assert_eq!(
res,
crate::graphql_value!({
"name": "Subscription",
"description": "Subscription Description.",
"fields": [
{
"name": "withSelf",
"description": "With Self Description",
"args": [],
},
{
"name": "independent",
"description": None,
"args": [],
},
{
"name": "withExecutor",
"description": None,
"args": [],
},
{
"name": "withExecutorAndSelf",
"description": None,
"args": [],
},
{
"name": "withContext",
"description": None,
"args": [],
},
{
"name": "withContextAndSelf",
"description": None,
"args": [],
},
{
"name": "renamed",
"description": None,
"args": [],
},
{
"name": "hasDescriptionAttr",
"description": "attr",
"args": [],
},
{
"name": "hasDescriptionDocComment",
"description": "Doc description",
"args": [],
},
{
"name": "hasArgument",
"description": None,
"args": [
{
"name": "arg1",
"description": None,
"type": {
"name": None,
},
}
],
},
{
"name": "defaultArgument",
"description": None,
"args": [
{
"name": "defaultArg",
"description": None,
"type": {
"name": "Boolean",
},
}
],
},
{
"name": "argWithDescription",
"description": None,
"args": [
{
"name": "arg",
"description": "my argument description",
"type": {
"name": None
},
}
],
},
{
"name": "withContextChild",
"description": None,
"args": [],
},
{
"name": "withImplicitLifetimeChild",
"description": None,
"args": [],
},
{
"name": "withMutArg",
"description": None,
"args": [
{
"name": "arg",
"description": None,
"type": {
"name": None,
},
}
],
},
{
"name": "withoutTypeAlias",
"description": None,
"args": [],
}
]
})
);
}
#[tokio::test]
async fn object_query() {
let doc = r#"
subscription {
withSelf
independent
withExecutor
withExecutorAndSelf
withContext
withContextAndSelf
renamed
hasArgument(arg1: true)
defaultArgument
argWithDescription(arg: true)
withContextChild {
ctx
}
withImplicitLifetimeChild {
value
}
withMutArg(arg: true)
withoutTypeAlias
}
"#;
let schema = RootNode::new(
Query,
EmptyMutation::<Context>::new(),
Subscription { b: true },
);
let vars = std::collections::HashMap::new();
let (stream_val, errs) =
crate::resolve_into_stream(doc, None, &schema, &vars, &Context { flag1: true })
.await
.expect("Execution failed");
let result = if let Value::Object(obj) = stream_val {
let mut result = Vec::new();
for (name, mut val) in obj {
if let Value::Scalar(ref mut stream) = val {
let first = stream
.next()
.await
.expect("Stream does not have the first element")
.expect(&format!("Error resolving {} field", name));
result.push((name, first))
}
}
result
} else {
panic!("Expected to get Value::Object ")
};
assert_eq!(errs, []);
assert_eq!(
result,
vec![
("withSelf".to_string(), graphql_value!(true)),
("independent".to_string(), graphql_value!(100)),
("withExecutor".to_string(), graphql_value!(true)),
("withExecutorAndSelf".to_string(), graphql_value!(true)),
("withContext".to_string(), graphql_value!(true)),
("withContextAndSelf".to_string(), graphql_value!(true)),
("renamed".to_string(), graphql_value!(true)),
("hasArgument".to_string(), graphql_value!(true)),
("defaultArgument".to_string(), graphql_value!(true)),
("argWithDescription".to_string(), graphql_value!(true)),
(
"withContextChild".to_string(),
graphql_value!({"ctx": true})
),
(
"withImplicitLifetimeChild".to_string(),
graphql_value!({ "value": "blub" })
),
("withMutArg".to_string(), graphql_value!(false)),
("withoutTypeAlias".to_string(), graphql_value!("abc")),
]
);
}

View file

@ -3,7 +3,7 @@ use std::marker::PhantomData;
use crate::{
ast::InputValue,
schema::model::RootNode,
types::scalars::EmptyMutation,
types::scalars::{EmptyMutation, EmptySubscription},
value::{DefaultScalarValue, Object, Value},
};
@ -166,7 +166,11 @@ where
}
}
"#;
let schema = RootNode::new(Root {}, EmptyMutation::<()>::new());
let schema = RootNode::new(
Root {},
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let vars = vec![("typeName".to_owned(), InputValue::scalar(type_name))]
.into_iter()
.collect();

View file

@ -1,6 +1,7 @@
mod args;
mod field;
mod impl_object;
mod impl_subscription;
mod interface;
mod object;
mod scalar;

View file

@ -1,5 +1,5 @@
// TODO: make sure proc macro tests cover all
// variants of the below
// variants of the below
/*
use std::marker::PhantomData;
@ -8,7 +8,7 @@ use crate::{
ast::InputValue,
executor::{Context, FieldResult},
schema::model::RootNode,
types::scalars::EmptyMutation,
types::scalars::{EmptyMutation, EmptySubscription},
value::{DefaultScalarValue, Object, Value},
};
@ -167,7 +167,11 @@ where
}
}
"#;
let schema = RootNode::new(Root {}, EmptyMutation::<InnerContext>::new());
let schema = RootNode::new(
Root {},
EmptyMutation::<InnerContext>::new(),
EmptySubscription::<()>::new(),
);
let vars = vec![("typeName".to_owned(), InputValue::scalar(type_name))]
.into_iter()
.collect();

View file

@ -1,7 +1,7 @@
use crate::{
executor::Variables,
schema::model::RootNode,
types::scalars::EmptyMutation,
types::scalars::{EmptyMutation, EmptySubscription},
value::{DefaultScalarValue, Object, ParseScalarResult, ParseScalarValue, Value},
};
@ -100,7 +100,11 @@ async fn run_type_info_query<F>(doc: &str, f: F)
where
F: Fn(&Object<DefaultScalarValue>) -> (),
{
let schema = RootNode::new(Root {}, EmptyMutation::<()>::new());
let schema = RootNode::new(
Root {},
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let (result, errs) = crate::execute(doc, None, &schema, &Variables::new(), &())
.await

View file

@ -15,7 +15,7 @@ use std::marker::PhantomData;
use crate::{
ast::InputValue,
schema::model::RootNode,
types::scalars::EmptyMutation,
types::scalars::{EmptyMutation, EmptySubscription},
value::{DefaultScalarValue, Object, Value},
};
@ -121,7 +121,11 @@ where
}
}
"#;
let schema = RootNode::new(Root {}, EmptyMutation::<()>::new());
let schema = RootNode::new(
Root {},
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let vars = vec![("typeName".to_owned(), InputValue::scalar(type_name))]
.into_iter()
.collect();

View file

@ -1,13 +1,21 @@
use crate::{DefaultScalarValue, GraphQLTypeAsync, RootNode, Value, Variables};
use std::default::Default;
pub async fn run_query<Query, Mutation, Context>(query: &str) -> Value
pub async fn run_query<Query, Mutation, Subscription, Context>(query: &str) -> Value
where
Query: GraphQLTypeAsync<DefaultScalarValue, TypeInfo = (), Context = Context> + Default,
Mutation: GraphQLTypeAsync<DefaultScalarValue, TypeInfo = (), Context = Context> + Default,
Context: Default + Sync + Send,
Subscription: crate::GraphQLType<DefaultScalarValue, TypeInfo = (), Context = Context>
+ Default
+ Sync
+ Send,
Context: Default + Send + Sync,
{
let schema = RootNode::new(Query::default(), Mutation::default());
let schema = RootNode::new(
Query::default(),
Mutation::default(),
Subscription::default(),
);
let (result, errs) =
crate::execute(query, None, &schema, &Variables::new(), &Context::default())
.await
@ -17,11 +25,15 @@ where
result
}
pub async fn run_info_query<Query, Mutation, Context>(type_name: &str) -> Value
pub async fn run_info_query<Query, Mutation, Subscription, Context>(type_name: &str) -> Value
where
Query: GraphQLTypeAsync<DefaultScalarValue, TypeInfo = (), Context = Context> + Default,
Mutation: GraphQLTypeAsync<DefaultScalarValue, TypeInfo = (), Context = Context> + Default,
Context: Default + Sync + Send,
Subscription: crate::GraphQLType<DefaultScalarValue, TypeInfo = (), Context = Context>
+ Default
+ Sync
+ Send,
Context: Default + Send + Sync,
{
let query = format!(
r#"
@ -45,7 +57,7 @@ where
"#,
type_name
);
let result = run_query::<Query, Mutation, Context>(&query).await;
let result = run_query::<Query, Mutation, Subscription, Context>(&query).await;
result
.as_object_value()
.expect("Result is not an object")

View file

@ -4,8 +4,8 @@ use crate::{
},
parser::{document::parse_document_source, ParseError, SourcePosition, Spanning, Token},
schema::model::SchemaType,
types::scalars::EmptyMutation,
validation::test_harness::{MutationRoot, QueryRoot},
types::scalars::{EmptyMutation, EmptySubscription},
validation::test_harness::{MutationRoot, QueryRoot, SubscriptionRoot},
value::{DefaultScalarValue, ScalarValue},
};
@ -13,15 +13,21 @@ fn parse_document<S>(s: &str) -> Document<S>
where
S: ScalarValue,
{
parse_document_source(s, &SchemaType::new::<QueryRoot, MutationRoot>(&(), &()))
.expect(&format!("Parse error on input {:#?}", s))
parse_document_source(
s,
&SchemaType::new::<QueryRoot, MutationRoot, SubscriptionRoot>(&(), &(), &()),
)
.expect(&format!("Parse error on input {:#?}", s))
}
fn parse_document_error<'a, S>(s: &'a str) -> Spanning<ParseError<'a>>
where
S: ScalarValue,
{
match parse_document_source::<S>(s, &SchemaType::new::<QueryRoot, MutationRoot>(&(), &())) {
match parse_document_source::<S>(
s,
&SchemaType::new::<QueryRoot, MutationRoot, SubscriptionRoot>(&(), &(), &()),
) {
Ok(doc) => panic!("*No* parse error on input {:#?} =>\n{:#?}", s, doc),
Err(err) => err,
}
@ -156,7 +162,11 @@ fn issue_427_panic_is_not_expected() {
}
}
let schema = SchemaType::new::<QueryWithoutFloat, EmptyMutation<()>>(&(), &());
let schema = SchemaType::new::<QueryWithoutFloat, EmptyMutation<()>, EmptySubscription<()>>(
&(),
&(),
&(),
);
let parse_result = parse_document_source(r##"{ echo(value: 123.0) }"##, &schema);
assert_eq!(

View file

@ -15,7 +15,7 @@ use crate::{
meta::{Argument, EnumMeta, EnumValue, InputObjectMeta, MetaType, ScalarMeta},
model::SchemaType,
},
types::scalars::EmptyMutation,
types::scalars::{EmptyMutation, EmptySubscription},
};
#[derive(GraphQLEnum)]
@ -75,7 +75,7 @@ where
{
let mut lexer = Lexer::new(s);
let mut parser = Parser::new(&mut lexer).expect(&format!("Lexer error on input {:#?}", s));
let schema = SchemaType::new::<Query, EmptyMutation<()>>(&(), &());
let schema = SchemaType::new::<Query, EmptyMutation<()>, EmptySubscription<()>>(&(), &(), &());
parse_value_literal(&mut parser, false, &schema, Some(meta))
.expect(&format!("Parse error on input {:#?}", s))

View file

@ -14,11 +14,16 @@ use crate::{
/// Root query node of a schema
///
/// This brings the mutation and query types together, and provides the
/// predefined metadata fields.
/// This brings the mutation, subscription and query types together,
/// and provides the predefined metadata fields.
#[derive(Debug)]
pub struct RootNode<'a, QueryT: GraphQLType<S>, MutationT: GraphQLType<S>, S = DefaultScalarValue>
where
pub struct RootNode<
'a,
QueryT: GraphQLType<S>,
MutationT: GraphQLType<S>,
SubscriptionT: GraphQLType<S>,
S = DefaultScalarValue,
> where
S: ScalarValue,
{
#[doc(hidden)]
@ -30,6 +35,10 @@ where
#[doc(hidden)]
pub mutation_info: MutationT::TypeInfo,
#[doc(hidden)]
pub subscription_type: SubscriptionT,
#[doc(hidden)]
pub subscription_info: SubscriptionT::TypeInfo,
#[doc(hidden)]
pub schema: SchemaType<'a, S>,
}
@ -39,6 +48,7 @@ pub struct SchemaType<'a, S> {
pub(crate) types: FnvHashMap<Name, MetaType<'a, S>>,
query_type_name: String,
mutation_type_name: Option<String>,
subscription_type_name: Option<String>,
directives: FnvHashMap<String, DirectiveType<'a, S>>,
}
@ -74,25 +84,31 @@ pub enum DirectiveLocation {
InlineFragment,
}
impl<'a, QueryT, MutationT, S> RootNode<'a, QueryT, MutationT, S>
impl<'a, QueryT, MutationT, SubscriptionT, S> RootNode<'a, QueryT, MutationT, SubscriptionT, S>
where
S: ScalarValue + 'a,
QueryT: GraphQLType<S, TypeInfo = ()>,
MutationT: GraphQLType<S, TypeInfo = ()>,
SubscriptionT: GraphQLType<S, TypeInfo = ()>,
{
/// Construct a new root node from query and mutation nodes
/// Construct a new root node from query, mutation, and subscription nodes
///
/// If the schema should not support mutations, use the
/// `new` constructor instead.
pub fn new(query_obj: QueryT, mutation_obj: MutationT) -> Self {
RootNode::new_with_info(query_obj, mutation_obj, (), ())
pub fn new(
query_obj: QueryT,
mutation_obj: MutationT,
subscription_obj: SubscriptionT,
) -> Self {
RootNode::new_with_info(query_obj, mutation_obj, subscription_obj, (), (), ())
}
}
impl<'a, S, QueryT, MutationT> RootNode<'a, QueryT, MutationT, S>
impl<'a, S, QueryT, MutationT, SubscriptionT> RootNode<'a, QueryT, MutationT, SubscriptionT, S>
where
QueryT: GraphQLType<S>,
MutationT: GraphQLType<S>,
SubscriptionT: GraphQLType<S>,
S: ScalarValue + 'a,
{
/// Construct a new root node from query and mutation nodes,
@ -101,32 +117,43 @@ where
pub fn new_with_info(
query_obj: QueryT,
mutation_obj: MutationT,
subscription_obj: SubscriptionT,
query_info: QueryT::TypeInfo,
mutation_info: MutationT::TypeInfo,
subscription_info: SubscriptionT::TypeInfo,
) -> Self {
RootNode {
query_type: query_obj,
mutation_type: mutation_obj,
schema: SchemaType::new::<QueryT, MutationT>(&query_info, &mutation_info),
subscription_type: subscription_obj,
schema: SchemaType::new::<QueryT, MutationT, SubscriptionT>(
&query_info,
&mutation_info,
&subscription_info,
),
query_info,
mutation_info,
subscription_info,
}
}
}
impl<'a, S> SchemaType<'a, S> {
pub fn new<QueryT, MutationT>(
pub fn new<QueryT, MutationT, SubscriptionT>(
query_info: &QueryT::TypeInfo,
mutation_info: &MutationT::TypeInfo,
subscription_info: &SubscriptionT::TypeInfo,
) -> Self
where
S: ScalarValue + 'a,
QueryT: GraphQLType<S>,
MutationT: GraphQLType<S>,
SubscriptionT: GraphQLType<S>,
{
let mut directives = FnvHashMap::default();
let query_type_name: String;
let mutation_type_name: String;
let subscription_type_name: String;
let mut registry = Registry::new(FnvHashMap::default());
query_type_name = registry
@ -139,6 +166,11 @@ impl<'a, S> SchemaType<'a, S> {
.innermost_name()
.to_owned();
subscription_type_name = registry
.get_type::<SubscriptionT>(subscription_info)
.innermost_name()
.to_owned();
registry.get_type::<SchemaType<S>>(&());
directives.insert("skip".to_owned(), DirectiveType::new_skip(&mut registry));
@ -177,6 +209,11 @@ impl<'a, S> SchemaType<'a, S> {
} else {
None
},
subscription_type_name: if &subscription_type_name != "_EmptySubscription" {
Some(subscription_type_name)
} else {
None
},
directives,
}
}
@ -235,15 +272,21 @@ impl<'a, S> SchemaType<'a, S> {
}
pub fn subscription_type(&self) -> Option<TypeType<S>> {
// subscription is not yet in `RootNode`,
// so return `None` for now
None
if let Some(ref subscription_type_name) = self.subscription_type_name {
Some(
self.type_by_name(subscription_type_name)
.expect("Subscription type does not exist in schema"),
)
} else {
None
}
}
pub fn concrete_subscription_type(&self) -> Option<&MetaType<S>> {
// subscription is not yet in `RootNode`,
// so return `None` for now
None
self.subscription_type_name.as_ref().map(|name| {
self.concrete_type_by_name(name)
.expect("Subscription type does not exist in schema")
})
}
pub fn type_list(&self) -> Vec<TypeType<S>> {

View file

@ -13,11 +13,13 @@ use crate::schema::{
model::{DirectiveLocation, DirectiveType, RootNode, SchemaType, TypeType},
};
impl<'a, CtxT, S, QueryT, MutationT> GraphQLType<S> for RootNode<'a, QueryT, MutationT, S>
impl<'a, CtxT, S, QueryT, MutationT, SubscriptionT> GraphQLType<S>
for RootNode<'a, QueryT, MutationT, SubscriptionT, S>
where
S: ScalarValue,
QueryT: GraphQLType<S, Context = CtxT>,
MutationT: GraphQLType<S, Context = CtxT>,
SubscriptionT: GraphQLType<S, Context = CtxT>,
{
type Context = CtxT;
type TypeInfo = QueryT::TypeInfo;
@ -75,15 +77,17 @@ where
}
}
impl<'a, CtxT, S, QueryT, MutationT> crate::GraphQLTypeAsync<S>
for RootNode<'a, QueryT, MutationT, S>
impl<'a, CtxT, S, QueryT, MutationT, SubscriptionT> crate::GraphQLTypeAsync<S>
for RootNode<'a, QueryT, MutationT, SubscriptionT, S>
where
S: ScalarValue + Send + Sync,
QueryT: crate::GraphQLTypeAsync<S, Context = CtxT>,
QueryT::TypeInfo: Send + Sync,
MutationT: crate::GraphQLTypeAsync<S, Context = CtxT>,
MutationT::TypeInfo: Send + Sync,
CtxT: Send + Sync,
SubscriptionT: GraphQLType<S, Context = CtxT> + Send + Sync,
SubscriptionT::TypeInfo: Send + Sync,
CtxT: Send + Sync + 'a,
{
fn resolve_field_async<'b>(
&'b self,
@ -121,7 +125,10 @@ where
.into_iter()
.filter(|t| {
t.to_concrete()
.map(|t| t.name() != Some("_EmptyMutation"))
.map(|t| {
!(t.name() == Some("_EmptyMutation")
|| t.name() == Some("_EmptySubscription"))
})
.unwrap_or(false)
})
.collect::<Vec<_>>()

View file

@ -6,7 +6,7 @@ use crate::{
introspection::IntrospectionFormat,
schema::model::RootNode,
tests::{model::Database, schema::Query},
types::scalars::EmptyMutation,
types::scalars::{EmptyMutation, EmptySubscription},
};
#[tokio::test]
@ -20,7 +20,11 @@ async fn test_introspection_query_type_name() {
}
}"#;
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
assert_eq!(
crate::execute(doc, None, &schema, &Variables::new(), &database).await,
@ -47,7 +51,11 @@ async fn test_introspection_type_name() {
}
}"#;
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
assert_eq!(
crate::execute(doc, None, &schema, &Variables::new(), &database).await,
@ -73,7 +81,11 @@ async fn test_introspection_specific_object_type_name_and_kind() {
}
"#;
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
assert_eq!(
crate::execute(doc, None, &schema, &Variables::new(), &database).await,
@ -100,7 +112,11 @@ async fn test_introspection_specific_interface_type_name_and_kind() {
}
"#;
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
assert_eq!(
crate::execute(doc, None, &schema, &Variables::new(), &database).await,
@ -127,7 +143,11 @@ async fn test_introspection_documentation() {
}
"#;
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
assert_eq!(
crate::execute(doc, None, &schema, &Variables::new(), &database).await,
@ -157,7 +177,11 @@ async fn test_introspection_directives() {
"#;
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
let mut result = crate::execute(q, None, &schema, &Variables::new(), &database)
.await
@ -203,7 +227,11 @@ async fn test_introspection_possible_types() {
}
"#;
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
let result = crate::execute(doc, None, &schema, &Variables::new(), &database).await;
@ -239,7 +267,11 @@ async fn test_introspection_possible_types() {
#[tokio::test]
async fn test_builtin_introspection_query() {
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
let mut result = crate::introspect(&schema, &database, IntrospectionFormat::default()).unwrap();
sort_schema_value(&mut result.0);
let expected = schema_introspection_result();
@ -249,7 +281,11 @@ async fn test_builtin_introspection_query() {
#[tokio::test]
async fn test_builtin_introspection_query_without_descriptions() {
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
let mut result =
crate::introspect(&schema, &database, IntrospectionFormat::WithoutDescriptions).unwrap();

View file

@ -9,4 +9,6 @@ pub mod schema;
#[cfg(test)]
mod schema_introspection;
#[cfg(test)]
mod subscriptions;
#[cfg(test)]
mod type_info_tests;

View file

@ -3,7 +3,7 @@ use crate::{
executor::Variables,
schema::model::RootNode,
tests::{model::Database, schema::Query},
types::scalars::EmptyMutation,
types::scalars::{EmptyMutation, EmptySubscription},
value::Value,
};
@ -16,7 +16,11 @@ async fn test_hero_name() {
}
}"#;
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
assert_eq!(
crate::execute(doc, None, &schema, &Variables::new(), &database).await,
@ -37,7 +41,11 @@ async fn test_hero_name() {
#[tokio::test]
async fn test_hero_field_order() {
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
let doc = r#"
{
@ -111,7 +119,11 @@ async fn test_hero_name_and_friends() {
}
}"#;
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
assert_eq!(
crate::execute(doc, None, &schema, &Variables::new(), &database).await,
@ -173,7 +185,11 @@ async fn test_hero_name_and_friends_and_friends_of_friends() {
}
}"#;
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
assert_eq!(
crate::execute(doc, None, &schema, &Variables::new(), &database).await,
@ -334,7 +350,11 @@ async fn test_hero_name_and_friends_and_friends_of_friends() {
async fn test_query_name() {
let doc = r#"{ human(id: "1000") { name } }"#;
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
assert_eq!(
crate::execute(doc, None, &schema, &Variables::new(), &database).await,
@ -360,7 +380,11 @@ async fn test_query_name() {
async fn test_query_alias_single() {
let doc = r#"{ luke: human(id: "1000") { name } }"#;
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
assert_eq!(
crate::execute(doc, None, &schema, &Variables::new(), &database).await,
@ -390,7 +414,11 @@ async fn test_query_alias_multiple() {
leia: human(id: "1003") { name }
}"#;
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
assert_eq!(
crate::execute(doc, None, &schema, &Variables::new(), &database).await,
@ -435,7 +463,11 @@ async fn test_query_alias_multiple_with_fragment() {
homePlanet
}"#;
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
assert_eq!(
crate::execute(doc, None, &schema, &Variables::new(), &database).await,
@ -477,7 +509,11 @@ async fn test_query_alias_multiple_with_fragment() {
async fn test_query_name_variable() {
let doc = r#"query FetchSomeIDQuery($someId: String!) { human(id: $someId) { name } }"#;
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
let vars = vec![("someId".to_owned(), InputValue::scalar("1000"))]
.into_iter()
@ -507,7 +543,11 @@ async fn test_query_name_variable() {
async fn test_query_name_invalid_variable() {
let doc = r#"query FetchSomeIDQuery($someId: String!) { human(id: $someId) { name } }"#;
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
let vars = vec![("someId".to_owned(), InputValue::scalar("some invalid id"))]
.into_iter()
@ -526,7 +566,11 @@ async fn test_query_name_invalid_variable() {
async fn test_query_friends_names() {
let doc = r#"{ human(id: "1000") { friends { name } } }"#;
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
assert_eq!(
crate::execute(doc, None, &schema, &Variables::new(), &database).await,
@ -583,7 +627,11 @@ async fn test_query_inline_fragments_droid() {
}
"#;
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
assert_eq!(
crate::execute(doc, None, &schema, &Variables::new(), &database).await,
@ -620,7 +668,11 @@ async fn test_query_inline_fragments_human() {
}
"#;
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
assert_eq!(
crate::execute(doc, None, &schema, &Variables::new(), &database).await,
@ -654,7 +706,11 @@ async fn test_object_typename() {
}
}"#;
let database = Database::new();
let schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
assert_eq!(
crate::execute(doc, None, &schema, &Variables::new(), &database).await,

View file

@ -0,0 +1,365 @@
use std::{iter, iter::FromIterator as _, pin::Pin};
use futures::{self, StreamExt as _};
use juniper_codegen::GraphQLObjectInternal;
use crate::{
http::GraphQLRequest, Context, DefaultScalarValue, EmptyMutation, ExecutionError, FieldError,
Object, RootNode, Value,
};
#[derive(Debug, Clone)]
pub struct MyContext(i32);
impl Context for MyContext {}
#[derive(GraphQLObjectInternal)]
#[graphql(description = "A humanoid creature in the Star Wars universe")]
#[derive(Clone)]
struct Human {
id: String,
name: String,
home_planet: String,
}
struct MyQuery;
#[crate::graphql_object_internal(context = MyContext)]
impl MyQuery {}
type Schema =
RootNode<'static, MyQuery, EmptyMutation<MyContext>, MySubscription, DefaultScalarValue>;
fn run<O>(f: impl std::future::Future<Output = O>) -> O {
let mut rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(f)
}
type HumanStream = Pin<Box<dyn futures::Stream<Item = Human> + Send>>;
struct MySubscription;
#[crate::graphql_subscription_internal(context = MyContext)]
impl MySubscription {
async fn async_human() -> HumanStream {
Box::pin(futures::stream::once(async {
Human {
id: "stream id".to_string(),
name: "stream name".to_string(),
home_planet: "stream home planet".to_string(),
}
}))
}
async fn error_human() -> Result<HumanStream, FieldError> {
Err(FieldError::new(
"handler error",
Value::Scalar(DefaultScalarValue::String("more details".to_string())),
))
}
async fn human_with_context(ctxt: &MyContext) -> HumanStream {
let context_val = ctxt.0.clone();
Box::pin(futures::stream::once(async move {
Human {
id: context_val.to_string(),
name: context_val.to_string(),
home_planet: context_val.to_string(),
}
}))
}
async fn human_with_args(id: String, name: String) -> HumanStream {
Box::pin(futures::stream::once(async {
Human {
id,
name,
home_planet: "default home planet".to_string(),
}
}))
}
}
/// Create all variables, execute subscription
/// and collect returned iterators.
/// Panics if query is invalid (GraphQLError is returned)
fn create_and_execute(
query: String,
) -> Result<
(
Vec<String>,
Vec<Vec<Result<Value<DefaultScalarValue>, ExecutionError<DefaultScalarValue>>>>,
),
Vec<ExecutionError<DefaultScalarValue>>,
> {
let request = GraphQLRequest::new(query, None, None);
let root_node = Schema::new(MyQuery, EmptyMutation::new(), MySubscription);
let context = MyContext(2);
let response = run(crate::http::resolve_into_stream(
&request, &root_node, &context,
));
assert!(response.is_ok());
let (values, errors) = response.unwrap();
if errors.len() > 0 {
return Err(errors);
}
// cannot compare with `assert_eq` because
// stream does not implement Debug
let response_value_object = match values {
Value::Object(o) => Some(o),
_ => None,
};
assert!(response_value_object.is_some());
let response_returned_object = response_value_object.unwrap();
let fields = response_returned_object.into_iter();
let mut names = vec![];
let mut collected_values = vec![];
for (name, stream_val) in fields {
names.push(name.clone());
// since macro returns Value::Scalar(iterator) every time,
// other variants may be skipped
match stream_val {
Value::Scalar(stream) => {
let collected = run(stream.collect::<Vec<_>>());
collected_values.push(collected);
}
_ => unreachable!(),
}
}
Ok((names, collected_values))
}
#[test]
fn returns_requested_object() {
let query = r#"subscription {
asyncHuman(id: "1") {
id
name
}
}"#
.to_string();
let (names, collected_values) = create_and_execute(query).expect("Got error from stream");
let mut iterator_count = 0;
let expected_values = vec![vec![Ok(Value::Object(Object::from_iter(
std::iter::from_fn(move || {
iterator_count += 1;
match iterator_count {
1 => Some((
"id",
Value::Scalar(DefaultScalarValue::String("stream id".to_string())),
)),
2 => Some((
"name",
Value::Scalar(DefaultScalarValue::String("stream name".to_string())),
)),
_ => None,
}
}),
)))]];
assert_eq!(names, vec!["asyncHuman"]);
assert_eq!(collected_values, expected_values);
}
#[test]
fn returns_error() {
let query = r#"subscription {
errorHuman(id: "1") {
id
name
}
}"#
.to_string();
let response = create_and_execute(query);
assert!(response.is_err());
let returned_errors = response.err().unwrap();
let expected_error = ExecutionError::new(
crate::parser::SourcePosition::new(23, 1, 8),
&vec!["errorHuman"],
FieldError::new(
"handler error",
Value::Scalar(DefaultScalarValue::String("more details".to_string())),
),
);
assert_eq!(returned_errors, vec![expected_error]);
}
#[test]
fn can_access_context() {
let query = r#"subscription {
humanWithContext {
id
}
}"#
.to_string();
let (names, collected_values) = create_and_execute(query).expect("Got error from stream");
let mut iterator_count = 0;
let expected_values = vec![vec![Ok(Value::Object(Object::from_iter(iter::from_fn(
move || {
iterator_count += 1;
match iterator_count {
1 => Some((
"id",
Value::Scalar(DefaultScalarValue::String("2".to_string())),
)),
_ => None,
}
},
))))]];
assert_eq!(names, vec!["humanWithContext"]);
assert_eq!(collected_values, expected_values);
}
#[test]
fn resolves_typed_inline_fragments() {
let query = r#"subscription {
... on MySubscription {
asyncHuman(id: "32") {
id
}
}
}"#
.to_string();
let (names, collected_values) = create_and_execute(query).expect("Got error from stream");
let mut iterator_count = 0;
let expected_values = vec![vec![Ok(Value::Object(Object::from_iter(iter::from_fn(
move || {
iterator_count += 1;
match iterator_count {
1 => Some((
"id",
Value::Scalar(DefaultScalarValue::String("stream id".to_string())),
)),
_ => None,
}
},
))))]];
assert_eq!(names, vec!["asyncHuman"]);
assert_eq!(collected_values, expected_values);
}
#[test]
fn resolves_nontyped_inline_fragments() {
let query = r#"subscription {
... {
asyncHuman(id: "32") {
id
}
}
}"#
.to_string();
let (names, collected_values) = create_and_execute(query).expect("Got error from stream");
let mut iterator_count = 0;
let expected_values = vec![vec![Ok(Value::Object(Object::from_iter(iter::from_fn(
move || {
iterator_count += 1;
match iterator_count {
1 => Some((
"id",
Value::Scalar(DefaultScalarValue::String("stream id".to_string())),
)),
_ => None,
}
},
))))]];
assert_eq!(names, vec!["asyncHuman"]);
assert_eq!(collected_values, expected_values);
}
#[test]
fn can_access_arguments() {
let query = r#"subscription {
humanWithArgs(id: "123", name: "args name") {
id
name
}
}"#
.to_string();
let (names, collected_values) = create_and_execute(query).expect("Got error from stream");
let mut iterator_count = 0;
let expected_values = vec![vec![Ok(Value::Object(Object::from_iter(iter::from_fn(
move || {
iterator_count += 1;
match iterator_count {
1 => Some((
"id",
Value::Scalar(DefaultScalarValue::String("123".to_string())),
)),
2 => Some((
"name",
Value::Scalar(DefaultScalarValue::String("args name".to_string())),
)),
_ => None,
}
},
))))]];
assert_eq!(names, vec!["humanWithArgs"]);
assert_eq!(collected_values, expected_values);
}
#[test]
fn type_alias() {
let query = r#"subscription {
aliasedHuman: asyncHuman(id: "1") {
id
name
}
}"#
.to_string();
let (names, collected_values) = create_and_execute(query).expect("Got error from stream");
let mut iterator_count = 0;
let expected_values = vec![vec![Ok(Value::Object(Object::from_iter(iter::from_fn(
move || {
iterator_count += 1;
match iterator_count {
1 => Some((
"id",
Value::Scalar(DefaultScalarValue::String("stream id".to_string())),
)),
2 => Some((
"name",
Value::Scalar(DefaultScalarValue::String("stream name".to_string())),
)),
_ => None,
}
},
))))]];
assert_eq!(names, vec!["aliasedHuman"]);
assert_eq!(collected_values, expected_values);
}

View file

@ -5,7 +5,7 @@ use crate::{
schema::{meta::MetaType, model::RootNode},
types::{
base::{Arguments, GraphQLType},
scalars::EmptyMutation,
scalars::{EmptyMutation, EmptySubscription},
},
value::{ScalarValue, Value},
};
@ -74,7 +74,14 @@ fn test_node() {
node.attributes.insert("foo".to_string(), "1".to_string());
node.attributes.insert("bar".to_string(), "2".to_string());
node.attributes.insert("baz".to_string(), "3".to_string());
let schema: RootNode<_, _> = RootNode::new_with_info(node, EmptyMutation::new(), node_info, ());
let schema: RootNode<_, _, _> = RootNode::new_with_info(
node,
EmptyMutation::new(),
EmptySubscription::new(),
node_info,
(),
(),
);
assert_eq!(
crate::execute_sync(doc, None, &schema, &Variables::new(), &()),

View file

@ -1,18 +1,20 @@
use crate::{
ast::Selection,
value::{Object, ScalarValue, Value},
};
use crate::{
executor::{ExecutionResult, Executor},
parser::Spanning,
value::{Object, ScalarValue, Value},
};
use crate::BoxFuture;
use super::base::{is_excluded, merge_key_into, Arguments, GraphQLType};
/// TODO: docs.
/**
This trait extends `GraphQLType` with asynchronous queries/mutations resolvers.
Convenience macros related to asynchronous queries/mutations expand into an
implementation of this trait and `GraphQLType` for the given type.
*/
pub trait GraphQLTypeAsync<S>: GraphQLType<S> + Send + Sync
where
Self::Context: Send + Sync,
@ -77,7 +79,7 @@ where
info: &'a Self::TypeInfo,
type_name: &str,
selection_set: Option<&'a [Selection<'a, S>]>,
executor: &'a Executor<'a, Self::Context, S>,
executor: &'a Executor<'a, 'a, Self::Context, S>,
) -> BoxFuture<'a, ExecutionResult<S>> {
if Self::name(info).unwrap() == type_name {
self.resolve_async(info, selection_set, executor)
@ -89,11 +91,11 @@ where
// Wrapper function around resolve_selection_set_into_async_recursive.
// This wrapper is necessary because async fns can not be recursive.
pub(crate) fn resolve_selection_set_into_async<'a, 'e, T, CtxT, S>(
fn resolve_selection_set_into_async<'a, 'e, T, CtxT, S>(
instance: &'a T,
info: &'a T::TypeInfo,
selection_set: &'e [Selection<'e, S>],
executor: &'e Executor<'e, CtxT, S>,
executor: &'e Executor<'e, 'e, CtxT, S>,
) -> BoxFuture<'a, Value<S>>
where
T: GraphQLTypeAsync<S, Context = CtxT>,
@ -124,7 +126,7 @@ pub(crate) async fn resolve_selection_set_into_async_recursive<'a, T, CtxT, S>(
instance: &'a T,
info: &'a T::TypeInfo,
selection_set: &'a [Selection<'a, S>],
executor: &'a Executor<'a, CtxT, S>,
executor: &'a Executor<'a, 'a, CtxT, S>,
) -> Value<S>
where
T: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
@ -132,7 +134,7 @@ where
S: ScalarValue + Send + Sync,
CtxT: Send + Sync,
{
use futures::stream::{FuturesOrdered, StreamExt};
use futures::stream::{FuturesOrdered, StreamExt as _};
let mut object = Object::with_capacity(selection_set.len());
@ -232,8 +234,6 @@ where
continue;
}
println!("WHATEVR");
// TODO: prevent duplicate boxing.
let f = async move {
let fragment = &executor

View file

@ -4,14 +4,10 @@ use juniper_codegen::GraphQLEnumInternal as GraphQLEnum;
use crate::{
ast::{Directive, FromInputValue, InputValue, Selection},
executor::Variables,
value::{DefaultScalarValue, Object, ScalarValue, Value},
};
use crate::{
executor::{ExecutionResult, Executor, Registry},
executor::{ExecutionResult, Executor, Registry, Variables},
parser::Spanning,
schema::meta::{Argument, MetaType},
value::{DefaultScalarValue, Object, ScalarValue, Value},
};
/// GraphQL type kind
@ -345,6 +341,12 @@ where
}
}
/// Resolver logic for queries'/mutations' selection set.
/// Calls appropriate resolver method for each field or fragment found
/// and then merges returned values into `result` or pushes errors to
/// field's/fragment's sub executor.
///
/// Returns false if any errors occured and true otherwise.
pub(crate) fn resolve_selection_set_into<T, CtxT, S>(
instance: &T,
info: &T::TypeInfo,
@ -531,6 +533,7 @@ where
false
}
/// Merges `response_name`/`value` pair into `result`
pub(crate) fn merge_key_into<S>(result: &mut Object<S>, response_name: &str, value: Value<S>) {
if let Some(&mut (_, ref mut e)) = result
.iter_mut()
@ -563,6 +566,7 @@ pub(crate) fn merge_key_into<S>(result: &mut Object<S>, response_name: &str, val
result.add_field(response_name, value);
}
/// Merges `src` object's fields into `dest`
fn merge_maps<S>(dest: &mut Object<S>, src: Object<S>) {
for (key, value) in src {
if dest.contains_field(&key) {

View file

@ -211,7 +211,7 @@ where
}
async fn resolve_into_list_async<'a, S, T, I>(
executor: &'a Executor<'a, T::Context, S>,
executor: &'a Executor<'a, 'a, T::Context, S>,
info: &'a T::TypeInfo,
items: I,
) -> ExecutionResult<S>
@ -222,7 +222,7 @@ where
T::TypeInfo: Send + Sync,
T::Context: Send + Sync,
{
use futures::stream::{FuturesOrdered, StreamExt};
use futures::stream::{FuturesOrdered, StreamExt as _};
use std::iter::FromIterator;
let stop_on_null = executor

View file

@ -1,8 +1,8 @@
pub mod async_await;
pub mod base;
pub mod containers;
pub mod name;
pub mod pointers;
pub mod scalars;
pub mod subscriptions;
pub mod utilities;
pub mod async_await;

View file

@ -219,8 +219,8 @@ impl<'e, S, T> crate::GraphQLTypeAsync<S> for std::sync::Arc<T>
where
S: ScalarValue + Send + Sync,
T: crate::GraphQLTypeAsync<S>,
<T as crate::types::base::GraphQLType<S>>::TypeInfo: Sync + Send,
<T as crate::types::base::GraphQLType<S>>::Context: Sync + Send,
<T as crate::types::base::GraphQLType<S>>::TypeInfo: Send + Sync,
<T as crate::types::base::GraphQLType<S>>::Context: Send + Sync,
{
fn resolve_async<'a>(
&'a self,

View file

@ -336,9 +336,58 @@ where
{
}
/// Utillity type to define read-only schemas
///
/// If you instantiate `RootNode` with this as the subscription,
/// no subscriptions will be generated for the schema.
pub struct EmptySubscription<T> {
phantom: PhantomData<T>,
}
// This is safe due to never using `T`.
unsafe impl<T> Send for EmptySubscription<T> {}
impl<T> EmptySubscription<T> {
/// Construct a new empty subscription
pub fn new() -> Self {
EmptySubscription {
phantom: PhantomData,
}
}
}
impl<S, T> GraphQLType<S> for EmptySubscription<T>
where
S: ScalarValue,
{
type Context = T;
type TypeInfo = ();
fn name(_: &()) -> Option<&str> {
Some("_EmptySubscription")
}
fn meta<'r>(_: &(), registry: &mut Registry<'r, S>) -> MetaType<'r, S>
where
S: 'r,
{
registry.build_object_type::<Self>(&(), &[]).into_meta()
}
}
impl<T, S> crate::GraphQLSubscriptionType<S> for EmptySubscription<T>
where
S: ScalarValue + Send + Sync + 'static,
Self: GraphQLType<S> + Send + Sync,
Self::TypeInfo: Send + Sync,
Self::Context: Send + Sync,
T: Send + Sync,
{
}
#[cfg(test)]
mod tests {
use super::{EmptyMutation, ID};
use super::{EmptyMutation, EmptySubscription, ID};
use crate::{
parser::ScalarToken,
value::{DefaultScalarValue, ParseScalarValue},
@ -391,4 +440,10 @@ mod tests {
fn check_if_send<T: Send>() {}
check_if_send::<EmptyMutation<()>>();
}
#[test]
fn empty_subscription_is_send() {
fn check_if_send<T: Send>() {}
check_if_send::<EmptySubscription<()>>();
}
}

View file

@ -0,0 +1,389 @@
use crate::{
http::{GraphQLRequest, GraphQLResponse},
parser::Spanning,
types::base::{is_excluded, merge_key_into},
Arguments, BoxFuture, Executor, FieldError, GraphQLType, Object, ScalarValue, Selection, Value,
ValuesStream,
};
/// Global subscription coordinator trait.
///
/// With regular queries we could get away with not having some in-between
/// layer, but for subscriptions it is needed, otherwise the integration crates
/// can become really messy and cumbersome to maintain. Subscriptions are also
/// quite a bit more stability sensitive than regular queries, they provide a
/// great vector for DOS attacks and can bring down a server easily if not
/// handled right.
///
/// This trait implementation might include the following features:
/// - contains the schema
/// - keeps track of subscription connections
/// - handles subscription start, maintains a global subscription id
/// - max subscription limits / concurrency limits
/// - subscription de-duplication
/// - reconnection on connection loss / buffering / re-synchronisation
///
///
/// `'a` is how long spawned connections live for.
pub trait SubscriptionCoordinator<'a, CtxT, S>
where
S: ScalarValue,
{
/// Type of [`SubscriptionConnection`]s this [`SubscriptionCoordinator`]
/// returns
type Connection: SubscriptionConnection<'a, S>;
/// Type of error while trying to spawn [`SubscriptionConnection`]
type Error;
/// Return [`SubscriptionConnection`] based on given [`GraphQLRequest`]
fn subscribe(
&'a self,
_: &'a GraphQLRequest<S>,
_: &'a CtxT,
) -> BoxFuture<'a, Result<Self::Connection, Self::Error>>;
}
/// Single subscription connection.
///
/// This trait implementation might:
/// - hold schema + context
/// - process subscribe, unsubscribe
/// - unregister from coordinator upon close/shutdown
/// - connection-local + global de-duplication, talk to coordinator
/// - concurrency limits
/// - machinery with coordinator to allow reconnection
///
/// It can be treated as [`futures::Stream`] yielding [`GraphQLResponse`]s in
/// server integration crates.
pub trait SubscriptionConnection<'a, S>: futures::Stream<Item = GraphQLResponse<'a, S>> {}
/**
This trait adds resolver logic with asynchronous subscription execution logic
on GraphQL types. It should be used with `GraphQLType` in order to implement
subscription resolvers on GraphQL objects.
Subscription-related convenience macros expand into an implementation of this
trait and `GraphQLType` for the given type.
See trait methods for more detailed explanation on how this trait works.
*/
pub trait GraphQLSubscriptionType<S>: GraphQLType<S> + Send + Sync
where
Self::Context: Send + Sync,
Self::TypeInfo: Send + Sync,
S: ScalarValue + Send + Sync,
{
/// Resolve into `Value<ValuesStream>`
///
/// ## Default implementation
///
/// In order to resolve selection set on object types, default
/// implementation calls `resolve_field_into_stream` every time a field
/// needs to be resolved and `resolve_into_type_stream` every time a
/// fragment needs to be resolved.
///
/// For non-object types, the selection set will be `None` and default
/// implementation will panic.
fn resolve_into_stream<'s, 'i, 'ref_e, 'e, 'res, 'f>(
&'s self,
info: &'i Self::TypeInfo,
executor: &'ref_e Executor<'ref_e, 'e, Self::Context, S>,
) -> BoxFuture<'f, Result<Value<ValuesStream<'res, S>>, FieldError<S>>>
where
'e: 'res,
'i: 'res,
's: 'f,
'ref_e: 'f,
'res: 'f,
{
if executor.current_selection_set().is_some() {
Box::pin(
async move { Ok(resolve_selection_set_into_stream(self, info, executor).await) },
)
} else {
panic!("resolve_into_stream() must be implemented");
}
}
/// This method is called by Self's `resolve_into_stream` default
/// implementation every time any field is found in selection set.
///
/// It replaces `GraphQLType::resolve_field`.
/// Unlike `resolve_field`, which resolves each field into a single
/// `Value<S>`, this method resolves each field into
/// `Value<ValuesStream<S>>`.
///
/// The default implementation panics.
fn resolve_field_into_stream<'s, 'i, 'ft, 'args, 'e, 'ref_e, 'res, 'f>(
&'s self,
_: &'i Self::TypeInfo, // this subscription's type info
_: &'ft str, // field's type name
_: Arguments<'args, S>, // field's arguments
_: &'ref_e Executor<'ref_e, 'e, Self::Context, S>, // field's executor (subscription's sub-executor
// with current field's selection set)
) -> BoxFuture<'f, Result<Value<ValuesStream<'res, S>>, FieldError<S>>>
where
's: 'f,
'i: 'res,
'ft: 'f,
'args: 'f,
'ref_e: 'f,
'res: 'f,
'e: 'res,
{
panic!("resolve_field_into_stream must be implemented");
}
/// This method is called by Self's `resolve_into_stream` default
/// implementation every time any fragment is found in selection set.
///
/// It replaces `GraphQLType::resolve_into_type`.
/// Unlike `resolve_into_type`, which resolves each fragment
/// a single `Value<S>`, this method resolves each fragment into
/// `Value<ValuesStream<S>>`.
///
/// The default implementation panics.
fn resolve_into_type_stream<'s, 'i, 'tn, 'e, 'ref_e, 'res, 'f>(
&'s self,
info: &'i Self::TypeInfo, // this subscription's type info
type_name: &'tn str, // fragment's type name
executor: &'ref_e Executor<'ref_e, 'e, Self::Context, S>, // fragment's executor (subscription's sub-executor
// with current field's selection set)
) -> BoxFuture<'f, Result<Value<ValuesStream<'res, S>>, FieldError<S>>>
where
'i: 'res,
'e: 'res,
's: 'f,
'tn: 'f,
'ref_e: 'f,
'res: 'f,
{
Box::pin(async move {
if Self::name(info) == Some(type_name) {
self.resolve_into_stream(info, executor).await
} else {
panic!("resolve_into_type_stream must be implemented");
}
})
}
}
/// Wrapper function around `resolve_selection_set_into_stream_recursive`.
/// This wrapper is necessary because async fns can not be recursive.
/// Panics if executor's current selection set is None.
pub(crate) fn resolve_selection_set_into_stream<'i, 'inf, 'ref_e, 'e, 'res, 'fut, T, CtxT, S>(
instance: &'i T,
info: &'inf T::TypeInfo,
executor: &'ref_e Executor<'ref_e, 'e, CtxT, S>,
) -> BoxFuture<'fut, Value<ValuesStream<'res, S>>>
where
'inf: 'res,
'e: 'res,
'i: 'fut,
'e: 'fut,
'ref_e: 'fut,
'res: 'fut,
T: GraphQLSubscriptionType<S, Context = CtxT>,
T::TypeInfo: Send + Sync,
S: ScalarValue + Send + Sync,
CtxT: Send + Sync,
{
Box::pin(resolve_selection_set_into_stream_recursive(
instance, info, executor,
))
}
/// Selection set default resolver logic.
/// Returns `Value::Null` if cannot keep resolving. Otherwise pushes errors to
/// `Executor`.
async fn resolve_selection_set_into_stream_recursive<'i, 'inf, 'ref_e, 'e, 'res, T, CtxT, S>(
instance: &'i T,
info: &'inf T::TypeInfo,
executor: &'ref_e Executor<'ref_e, 'e, CtxT, S>,
) -> Value<ValuesStream<'res, S>>
where
T: GraphQLSubscriptionType<S, Context = CtxT> + Send + Sync,
T::TypeInfo: Send + Sync,
S: ScalarValue + Send + Sync,
CtxT: Send + Sync,
'inf: 'res,
'e: 'res,
{
let selection_set = executor
.current_selection_set()
.expect("Executor's selection set is none");
let mut object: Object<ValuesStream<'res, S>> = Object::with_capacity(selection_set.len());
let meta_type = executor
.schema()
.concrete_type_by_name(
T::name(info)
.expect("Resolving named type's selection set")
.as_ref(),
)
.expect("Type not found in schema");
for selection in selection_set {
match selection {
Selection::Field(Spanning {
item: ref f,
start: ref start_pos,
..
}) => {
if is_excluded(&f.directives, &executor.variables()) {
continue;
}
let response_name = f.alias.as_ref().unwrap_or(&f.name).item;
if f.name.item == "__typename" {
let typename =
Value::scalar(instance.concrete_type_name(executor.context(), info));
object.add_field(
response_name,
Value::Scalar(Box::pin(futures::stream::once(async { Ok(typename) }))),
);
continue;
}
let meta_field = meta_type
.field_by_name(f.name.item)
.unwrap_or_else(|| {
panic!(format!(
"Field {} not found on type {:?}",
f.name.item,
meta_type.name()
))
})
.clone();
let exec_vars = executor.variables();
let sub_exec = executor.field_sub_executor(
response_name,
f.name.item,
start_pos.clone(),
f.selection_set.as_ref().map(|x| &x[..]),
);
let args = Arguments::new(
f.arguments.as_ref().map(|m| {
m.item
.iter()
.map(|&(ref k, ref v)| (k.item, v.item.clone().into_const(&exec_vars)))
.collect()
}),
&meta_field.arguments,
);
let is_non_null = meta_field.field_type.is_non_null();
let res = instance
.resolve_field_into_stream(info, f.name.item, args, &sub_exec)
.await;
match res {
Ok(Value::Null) if is_non_null => {
return Value::Null;
}
Ok(v) => merge_key_into(&mut object, response_name, v),
Err(e) => {
sub_exec.push_error_at(e, start_pos.clone());
if meta_field.field_type.is_non_null() {
return Value::Null;
}
object.add_field(f.name.item, Value::Null);
}
}
}
Selection::FragmentSpread(Spanning {
item: ref spread,
start: ref start_pos,
..
}) => {
if is_excluded(&spread.directives, &executor.variables()) {
continue;
}
let fragment = executor
.fragment_by_name(spread.name.item)
.expect("Fragment could not be found");
let sub_exec = executor.type_sub_executor(
Some(fragment.type_condition.item),
Some(&fragment.selection_set[..]),
);
let obj = instance
.resolve_into_type_stream(info, fragment.type_condition.item, &sub_exec)
.await;
match obj {
Ok(val) => {
match val {
Value::Object(o) => {
for (k, v) in o {
merge_key_into(&mut object, &k, v);
}
}
// since this was a wrapper of current function,
// we'll rather get an object or nothing
_ => unreachable!(),
}
}
Err(e) => sub_exec.push_error_at(e, start_pos.clone()),
}
}
Selection::InlineFragment(Spanning {
item: ref fragment,
start: ref start_pos,
..
}) => {
if is_excluded(&fragment.directives, &executor.variables()) {
continue;
}
let sub_exec = executor.type_sub_executor(
fragment.type_condition.as_ref().map(|c| c.item),
Some(&fragment.selection_set[..]),
);
if let Some(ref type_condition) = fragment.type_condition {
let sub_result = instance
.resolve_into_type_stream(info, type_condition.item, &sub_exec)
.await;
if let Ok(Value::Object(obj)) = sub_result {
for (k, v) in obj {
merge_key_into(&mut object, &k, v);
}
} else if let Err(e) = sub_result {
sub_exec.push_error_at(e, start_pos.clone());
}
} else {
if let Some(type_name) = meta_type.name() {
let sub_result = instance
.resolve_into_type_stream(info, type_name, &sub_exec)
.await;
if let Ok(Value::Object(obj)) = sub_result {
for (k, v) in obj {
merge_key_into(&mut object, &k, v);
}
} else if let Err(e) = sub_result {
sub_exec.push_error_at(e, start_pos.clone());
}
} else {
return Value::Null;
}
}
}
}
}
Value::Object(object)
}

View file

@ -742,7 +742,7 @@ mod tests {
schema::meta::MetaType,
types::{
base::GraphQLType,
scalars::{EmptyMutation, ID},
scalars::{EmptyMutation, EmptySubscription, ID},
},
};
@ -1694,9 +1694,17 @@ mod tests {
#[test]
fn compatible_return_shapes_on_different_return_types() {
expect_passes_rule_with_schema::<_, EmptyMutation<()>, _, _, DefaultScalarValue>(
expect_passes_rule_with_schema::<
_,
EmptyMutation<()>,
EmptySubscription<()>,
_,
_,
DefaultScalarValue,
>(
QueryRoot,
EmptyMutation::new(),
EmptySubscription::new(),
factory,
r#"
{
@ -1992,9 +2000,17 @@ mod tests {
#[test]
fn allows_non_conflicting_overlapping_types() {
expect_passes_rule_with_schema::<_, EmptyMutation<()>, _, _, DefaultScalarValue>(
expect_passes_rule_with_schema::<
_,
EmptyMutation<()>,
EmptySubscription<()>,
_,
_,
DefaultScalarValue,
>(
QueryRoot,
EmptyMutation::new(),
EmptySubscription::new(),
factory,
r#"
{
@ -2013,9 +2029,17 @@ mod tests {
#[test]
fn same_wrapped_scalar_return_types() {
expect_passes_rule_with_schema::<_, EmptyMutation<()>, _, _, DefaultScalarValue>(
expect_passes_rule_with_schema::<
_,
EmptyMutation<()>,
EmptySubscription<()>,
_,
_,
DefaultScalarValue,
>(
QueryRoot,
EmptyMutation::new(),
EmptySubscription::new(),
factory,
r#"
{
@ -2034,9 +2058,17 @@ mod tests {
#[test]
fn allows_inline_typeless_fragments() {
expect_passes_rule_with_schema::<_, EmptyMutation<()>, _, _, DefaultScalarValue>(
expect_passes_rule_with_schema::<
_,
EmptyMutation<()>,
EmptySubscription<()>,
_,
_,
DefaultScalarValue,
>(
QueryRoot,
EmptyMutation::new(),
EmptySubscription::new(),
factory,
r#"
{
@ -2104,9 +2136,17 @@ mod tests {
#[test]
fn ignores_unknown_types() {
expect_passes_rule_with_schema::<_, EmptyMutation<()>, _, _, DefaultScalarValue>(
expect_passes_rule_with_schema::<
_,
EmptyMutation<()>,
EmptySubscription<()>,
_,
_,
DefaultScalarValue,
>(
QueryRoot,
EmptyMutation::new(),
EmptySubscription::new(),
factory,
r#"
{

View file

@ -40,6 +40,8 @@ struct TestInput {
pub(crate) struct MutationRoot;
pub(crate) struct SubscriptionRoot;
#[derive(Debug)]
enum DogCommand {
Sit,
@ -625,15 +627,43 @@ where
}
}
pub fn validate<'a, Q, M, V, F, S>(r: Q, m: M, q: &'a str, factory: F) -> Vec<RuleError>
impl<S> GraphQLType<S> for SubscriptionRoot
where
S: ScalarValue,
{
type Context = ();
type TypeInfo = ();
fn name(_: &()) -> Option<&str> {
Some("SubscriptionRoot")
}
fn meta<'r>(i: &(), registry: &mut Registry<'r, S>) -> MetaType<'r, S>
where
S: 'r,
{
let fields = [];
registry.build_object_type::<Self>(i, &fields).into_meta()
}
}
pub fn validate<'a, Q, M, Sub, V, F, S>(
r: Q,
m: M,
s: Sub,
q: &'a str,
factory: F,
) -> Vec<RuleError>
where
S: ScalarValue + 'a,
Q: GraphQLType<S, TypeInfo = ()>,
M: GraphQLType<S, TypeInfo = ()>,
Sub: GraphQLType<S, TypeInfo = ()>,
V: Visitor<'a, S> + 'a,
F: Fn() -> V,
{
let mut root = RootNode::new(r, m);
let mut root = RootNode::new(r, m, s);
root.schema.add_directive(DirectiveType::new(
"onQuery",
@ -682,18 +712,24 @@ where
V: Visitor<'a, S> + 'a,
F: Fn() -> V,
{
expect_passes_rule_with_schema(QueryRoot, MutationRoot, factory, q);
expect_passes_rule_with_schema(QueryRoot, MutationRoot, SubscriptionRoot, factory, q);
}
pub fn expect_passes_rule_with_schema<'a, Q, M, V, F, S>(r: Q, m: M, factory: F, q: &'a str)
where
pub fn expect_passes_rule_with_schema<'a, Q, M, Sub, V, F, S>(
r: Q,
m: M,
s: Sub,
factory: F,
q: &'a str,
) where
S: ScalarValue + 'a,
Q: GraphQLType<S, TypeInfo = ()>,
M: GraphQLType<S, TypeInfo = ()>,
Sub: GraphQLType<S, TypeInfo = ()>,
V: Visitor<'a, S> + 'a,
F: Fn() -> V,
{
let errs = validate(r, m, q, factory);
let errs = validate(r, m, s, q, factory);
if !errs.is_empty() {
print_errors(&errs);
@ -723,7 +759,7 @@ pub fn expect_fails_rule_with_schema<'a, Q, M, V, F, S>(
V: Visitor<'a, S> + 'a,
F: Fn() -> V,
{
let errs = validate(r, m, q, factory);
let errs = validate(r, m, crate::EmptySubscription::<S>::new(), q, factory);
if errs.is_empty() {
panic!("Expected rule to fail, but no errors were found");

View file

@ -1,5 +1,6 @@
use juniper::{
graphql_object, DefaultScalarValue, ExecutionError, FieldError, GraphQLEnum, Value, Variables,
graphql_object, graphql_subscription, DefaultScalarValue, ExecutionError, FieldError,
GraphQLEnum, Value, Variables,
};
pub type QueryResult = Result<
@ -95,8 +96,13 @@ pub struct Mutation;
#[graphql_object(Context = Context)]
impl Mutation {}
pub fn new_schema() -> juniper::RootNode<'static, Query, Mutation> {
juniper::RootNode::new(Query, Mutation)
pub struct Subscription;
#[graphql_subscription(Context = Context)]
impl Subscription {}
pub fn new_schema() -> juniper::RootNode<'static, Query, Mutation, Subscription> {
juniper::RootNode::new(Query, Mutation, Subscription)
}
pub fn execute_sync(query: &str, vars: Variables) -> QueryResult {

View file

@ -4,6 +4,21 @@ use quote::quote;
/// Generate code for the juniper::graphql_object macro.
pub fn build_object(args: TokenStream, body: TokenStream, is_internal: bool) -> TokenStream {
let definition = create(args, body);
let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
definition.into_tokens(juniper_crate_name).into()
}
/// Generate code for the juniper::graphql_subscription macro.
pub fn build_subscription(args: TokenStream, body: TokenStream, is_internal: bool) -> TokenStream {
let definition = create(args, body);
let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
definition
.into_subscription_tokens(juniper_crate_name)
.into()
}
fn create(args: TokenStream, body: TokenStream) -> util::GraphQLTypeDefiniton {
let _impl = util::parse_impl::ImplBlock::parse(args, body);
let name = _impl
@ -160,6 +175,5 @@ pub fn build_object(args: TokenStream, body: TokenStream, is_internal: bool) ->
is_async,
});
}
let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
definition.into_tokens(juniper_crate_name).into()
definition
}

View file

@ -57,6 +57,12 @@ pub fn derive_object(input: TokenStream) -> TokenStream {
gen.into()
}
#[proc_macro_derive(GraphQLObjectInternal, attributes(graphql))]
pub fn derive_object_internal(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_object::build_derive_object(ast, true);
gen.into()
}
/// This custom derive macro implements the #[derive(GraphQLScalarValue)]
/// derive.
///
@ -379,6 +385,20 @@ pub fn graphql_object_internal(args: TokenStream, input: TokenStream) -> TokenSt
impl_object::build_object(args, input, true)
}
/// A proc macro for defining a GraphQL subscription.
#[proc_macro_attribute]
pub fn graphql_subscription(args: TokenStream, input: TokenStream) -> TokenStream {
let gen = impl_object::build_subscription(args, input, false);
gen.into()
}
#[doc(hidden)]
#[proc_macro_attribute]
pub fn graphql_subscription_internal(args: TokenStream, input: TokenStream) -> TokenStream {
let gen = impl_object::build_subscription(args, input, true);
gen.into()
}
#[proc_macro_attribute]
#[proc_macro_error::proc_macro_error]
pub fn graphql_union(attrs: TokenStream, body: TokenStream) -> TokenStream {

View file

@ -792,8 +792,7 @@ impl GraphQLTypeDefiniton {
let mut generics = self.generics.clone();
if self.scalar.is_some() {
} else if self.generic_scalar {
if self.scalar.is_none() && self.generic_scalar {
// No custom scalar specified, but always generic specified.
// Therefore we inject the generic scalar.
@ -985,6 +984,275 @@ impl GraphQLTypeDefiniton {
);
output
}
pub fn into_subscription_tokens(self, juniper_crate_name: &str) -> proc_macro2::TokenStream {
let juniper_crate_name = syn::parse_str::<syn::Path>(juniper_crate_name).unwrap();
let name = &self.name;
let ty = &self._type;
let context = self
.context
.as_ref()
.map(|ctx| quote!( #ctx ))
.unwrap_or_else(|| quote!(()));
let scalar = self
.scalar
.as_ref()
.map(|s| quote!( #s ))
.unwrap_or_else(|| {
if self.generic_scalar {
// If generic_scalar is true, we always insert a generic scalar.
// See more comments below.
quote!(__S)
} else {
quote!(#juniper_crate_name::DefaultScalarValue)
}
});
let field_definitions = self.fields.iter().map(|field| {
let args = field.args.iter().map(|arg| {
let arg_type = &arg._type;
let arg_name = &arg.name;
let description = match arg.description.as_ref() {
Some(value) => quote!( .description( #value ) ),
None => quote!(),
};
match arg.default.as_ref() {
Some(value) => quote!(
.argument(
registry.arg_with_default::<#arg_type>(#arg_name, &#value, info)
#description
)
),
None => quote!(
.argument(
registry.arg::<#arg_type>(#arg_name, info)
#description
)
),
}
});
let description = match field.description.as_ref() {
Some(description) => quote!( .description(#description) ),
None => quote!(),
};
let deprecation = match field.deprecation.as_ref() {
Some(deprecation) => {
if let Some(reason) = deprecation.reason.as_ref() {
quote!( .deprecated(Some(#reason)) )
} else {
quote!( .deprecated(None) )
}
}
None => quote!(),
};
let field_name = &field.name;
let type_name = &field._type;
let _type;
if field.is_async {
_type = quote!(<#type_name as #juniper_crate_name::ExtractTypeFromStream<_, #scalar>>::Item);
} else {
panic!("Synchronous resolvers are not supported. Specify that this function is async: 'async fn foo()'")
}
quote! {
registry
.field_convert::<#_type, _, Self::Context>(#field_name, info)
#(#args)*
#description
#deprecation
}
});
let description = self
.description
.as_ref()
.map(|description| quote!( .description(#description) ));
let interfaces = self.interfaces.as_ref().map(|items| {
quote!(
.interfaces(&[
#( registry.get_type::< #items >(&()) ,)*
])
)
});
// Preserve the original type_generics before modification,
// since alteration makes them invalid if self.generic_scalar
// is specified.
let (_, type_generics, _) = self.generics.split_for_impl();
let mut generics = self.generics.clone();
if self.scalar.is_none() && self.generic_scalar {
// No custom scalar specified, but always generic specified.
// Therefore we inject the generic scalar.
generics.params.push(parse_quote!(__S));
let where_clause = generics.where_clause.get_or_insert(parse_quote!(where));
// Insert ScalarValue constraint.
where_clause
.predicates
.push(parse_quote!(__S: #juniper_crate_name::ScalarValue));
}
let type_generics_tokens = if self.include_type_generics {
Some(type_generics)
} else {
None
};
let (impl_generics, _, where_clause) = generics.split_for_impl();
let resolve_matches_async = self.fields
.iter()
.filter(|field| field.is_async)
.map(|field| {
let name = &field.name;
let code = &field.resolver_code;
let _type;
if field.is_type_inferred {
_type = quote!();
} else {
let _type_name = &field._type;
_type = quote!(: #_type_name);
};
quote!(
#name => {
futures::FutureExt::boxed(async move {
let res #_type = { #code };
let res = #juniper_crate_name::IntoFieldResult::<_, #scalar>::into_result(res)?;
let executor= executor.as_owned_executor();
let f = res.then(move |res| {
let executor = executor.clone();
let res2: #juniper_crate_name::FieldResult<_, #scalar> =
#juniper_crate_name::IntoResolvable::into(res, executor.context());
async move {
let ex = executor.as_executor();
match res2 {
Ok(Some((ctx, r))) => {
let sub = ex.replaced_context(ctx);
sub.resolve_with_ctx_async(&(), &r)
.await
.map_err(|e| ex.new_error(e))
}
Ok(None) => Ok(Value::null()),
Err(e) => Err(ex.new_error(e)),
}
}
});
Ok(
#juniper_crate_name::Value::Scalar::<
#juniper_crate_name::ValuesStream
>(Box::pin(f))
)
})
}
)
});
let graphql_implementation = quote!(
impl#impl_generics #juniper_crate_name::GraphQLType<#scalar> for #ty #type_generics_tokens
#where_clause
{
type Context = #context;
type TypeInfo = ();
fn name(_: &Self::TypeInfo) -> Option<&str> {
Some(#name)
}
fn meta<'r>(
info: &Self::TypeInfo,
registry: &mut #juniper_crate_name::Registry<'r, #scalar>
) -> #juniper_crate_name::meta::MetaType<'r, #scalar>
where #scalar : 'r,
{
let fields = vec![
#( #field_definitions ),*
];
let meta = registry.build_object_type::<#ty>( info, &fields )
#description
#interfaces;
meta.into_meta()
}
fn resolve_field(
&self,
_: &(),
_: &str,
_: &#juniper_crate_name::Arguments<#scalar>,
_: &#juniper_crate_name::Executor<Self::Context, #scalar>,
) -> #juniper_crate_name::ExecutionResult<#scalar> {
panic!("Called `resolve_field` on subscription object");
}
fn concrete_type_name(&self, _: &Self::Context, _: &Self::TypeInfo) -> String {
#name.to_string()
}
}
);
let subscription_implementation = quote!(
impl#impl_generics #juniper_crate_name::GraphQLSubscriptionType<#scalar> for #ty #type_generics_tokens
#where_clause
{
#[allow(unused_variables)]
fn resolve_field_into_stream<
's, 'i, 'fi, 'args, 'e, 'ref_e, 'res, 'f,
>(
&'s self,
info: &'i Self::TypeInfo,
field_name: &'fi str,
args: #juniper_crate_name::Arguments<'args, #scalar>,
executor: &'ref_e #juniper_crate_name::Executor<'ref_e, 'e, Self::Context, #scalar>,
) -> std::pin::Pin<Box<
dyn futures::future::Future<
Output = Result<
#juniper_crate_name::Value<#juniper_crate_name::ValuesStream<'res, #scalar>>,
#juniper_crate_name::FieldError<#scalar>
>
> + Send + 'f
>>
where
's: 'f,
'i: 'res,
'fi: 'f,
'e: 'res,
'args: 'f,
'ref_e: 'f,
'res: 'f,
{
use #juniper_crate_name::Value;
use futures::stream::StreamExt as _;
match field_name {
#( #resolve_matches_async )*
_ => {
panic!("Field {} not found on type {}", field_name, "GraphQLSubscriptionType");
}
}
}
}
);
quote!(
#graphql_implementation
#subscription_implementation
)
}
}
#[cfg(test)]

View file

@ -4,7 +4,7 @@ use hyper::{
};
use juniper::{
tests::{model::Database, schema::Query},
EmptyMutation, RootNode,
EmptyMutation, EmptySubscription, RootNode,
};
use std::sync::Arc;
@ -15,7 +15,11 @@ async fn main() {
let addr = ([127, 0, 0, 1], 3000).into();
let db = Arc::new(Database::new());
let root_node = Arc::new(RootNode::new(Query, EmptyMutation::<Database>::new()));
let root_node = Arc::new(RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
));
let new_service = make_service_fn(move |_| {
let root_node = root_node.clone();

View file

@ -15,8 +15,8 @@ use serde_json::error::Error as SerdeError;
use std::{error::Error, fmt, string::FromUtf8Error, sync::Arc};
use url::form_urlencoded;
pub async fn graphql<CtxT, QueryT, MutationT, S>(
root_node: Arc<RootNode<'static, QueryT, MutationT, S>>,
pub async fn graphql<CtxT, QueryT, MutationT, SubscrtipionT, S>(
root_node: Arc<RootNode<'static, QueryT, MutationT, SubscrtipionT, S>>,
context: Arc<CtxT>,
request: Request<Body>,
) -> Result<Response<Body>, hyper::Error>
@ -25,8 +25,10 @@ where
CtxT: Send + Sync + 'static,
QueryT: GraphQLType<S, Context = CtxT> + Send + Sync + 'static,
MutationT: GraphQLType<S, Context = CtxT> + Send + Sync + 'static,
SubscrtipionT: GraphQLType<S, Context = CtxT> + Send + Sync + 'static,
QueryT::TypeInfo: Send + Sync,
MutationT::TypeInfo: Send + Sync,
SubscrtipionT::TypeInfo: Send + Sync,
{
match *request.method() {
Method::GET => {
@ -49,8 +51,8 @@ where
}
}
pub async fn graphql_async<CtxT, QueryT, MutationT, S>(
root_node: Arc<RootNode<'static, QueryT, MutationT, S>>,
pub async fn graphql_async<CtxT, QueryT, MutationT, SubscriptionT, S>(
root_node: Arc<RootNode<'static, QueryT, MutationT, SubscriptionT, S>>,
context: Arc<CtxT>,
request: Request<Body>,
) -> Result<Response<Body>, hyper::Error>
@ -59,8 +61,10 @@ where
CtxT: Send + Sync + 'static,
QueryT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync + 'static,
MutationT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync + 'static,
SubscriptionT: GraphQLType<S, Context = CtxT> + Send + Sync + 'static,
QueryT::TypeInfo: Send + Sync,
MutationT::TypeInfo: Send + Sync,
SubscriptionT::TypeInfo: Send + Sync,
{
match *request.method() {
Method::GET => {
@ -120,6 +124,7 @@ pub async fn playground(graphql_endpoint: &str) -> Result<Response<Body>, hyper:
let mut resp = new_html_response(StatusCode::OK);
*resp.body_mut() = Body::from(juniper::http::playground::playground_source(
graphql_endpoint,
None,
));
Ok(resp)
}
@ -131,8 +136,8 @@ fn render_error(err: GraphQLRequestError) -> Response<Body> {
resp
}
async fn execute_request<CtxT, QueryT, MutationT, S>(
root_node: Arc<RootNode<'static, QueryT, MutationT, S>>,
async fn execute_request<CtxT, QueryT, MutationT, SubscriptionT, S>(
root_node: Arc<RootNode<'static, QueryT, MutationT, SubscriptionT, S>>,
context: Arc<CtxT>,
request: GraphQLRequest<S>,
) -> Response<Body>
@ -141,8 +146,10 @@ where
CtxT: Send + Sync + 'static,
QueryT: GraphQLType<S, Context = CtxT> + Send + Sync + 'static,
MutationT: GraphQLType<S, Context = CtxT> + Send + Sync + 'static,
SubscriptionT: GraphQLType<S, Context = CtxT> + Send + Sync + 'static,
QueryT::TypeInfo: Send + Sync,
MutationT::TypeInfo: Send + Sync,
SubscriptionT::TypeInfo: Send + Sync,
{
let (is_ok, body) = request.execute_sync(root_node, context);
let code = if is_ok {
@ -159,8 +166,8 @@ where
resp
}
async fn execute_request_async<CtxT, QueryT, MutationT, S>(
root_node: Arc<RootNode<'static, QueryT, MutationT, S>>,
async fn execute_request_async<CtxT, QueryT, MutationT, SubscriptionT, S>(
root_node: Arc<RootNode<'static, QueryT, MutationT, SubscriptionT, S>>,
context: Arc<CtxT>,
request: GraphQLRequest<S>,
) -> Response<Body>
@ -169,8 +176,10 @@ where
CtxT: Send + Sync + 'static,
QueryT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync + 'static,
MutationT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync + 'static,
SubscriptionT: GraphQLType<S, Context = CtxT> + Send + Sync + 'static,
QueryT::TypeInfo: Send + Sync,
MutationT::TypeInfo: Send + Sync,
SubscriptionT::TypeInfo: Send + Sync,
{
let (is_ok, body) = request.execute(root_node, context).await;
let code = if is_ok {
@ -266,15 +275,16 @@ impl<S> GraphQLRequest<S>
where
S: ScalarValue,
{
fn execute_sync<'a, CtxT: 'a, QueryT, MutationT>(
fn execute_sync<'a, CtxT: 'a, QueryT, MutationT, SubscriptionT>(
self,
root_node: Arc<RootNode<'a, QueryT, MutationT, S>>,
root_node: Arc<RootNode<'a, QueryT, MutationT, SubscriptionT, S>>,
context: Arc<CtxT>,
) -> (bool, hyper::Body)
where
S: 'a + Send + Sync,
QueryT: GraphQLType<S, Context = CtxT> + 'a,
MutationT: GraphQLType<S, Context = CtxT> + 'a,
SubscriptionT: GraphQLType<S, Context = CtxT> + 'a,
{
match self {
GraphQLRequest::Single(request) => {
@ -303,17 +313,19 @@ where
}
}
async fn execute<'a, CtxT: 'a, QueryT, MutationT>(
async fn execute<'a, CtxT: 'a, QueryT, MutationT, SubscriptionT>(
self,
root_node: Arc<RootNode<'a, QueryT, MutationT, S>>,
root_node: Arc<RootNode<'a, QueryT, MutationT, SubscriptionT, S>>,
context: Arc<CtxT>,
) -> (bool, hyper::Body)
where
S: Send + Sync,
QueryT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
MutationT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
SubscriptionT: GraphQLType<S, Context = CtxT> + Send + Sync,
QueryT::TypeInfo: Send + Sync,
MutationT::TypeInfo: Send + Sync,
SubscriptionT::TypeInfo: Send + Sync,
CtxT: Send + Sync,
{
match self {
@ -385,7 +397,7 @@ mod tests {
use juniper::{
http::tests as http_tests,
tests::{model::Database, schema::Query},
EmptyMutation, RootNode,
EmptyMutation, EmptySubscription, RootNode,
};
use reqwest::{self, Response as ReqwestResponse};
use std::{net::SocketAddr, sync::Arc, thread, time::Duration};
@ -432,7 +444,11 @@ mod tests {
let addr: SocketAddr = ([127, 0, 0, 1], 3001).into();
let db = Arc::new(Database::new());
let root_node = Arc::new(RootNode::new(Query, EmptyMutation::<Database>::new()));
let root_node = Arc::new(RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
));
let new_service = make_service_fn(move |_| {
let root_node = root_node.clone();

View file

@ -10,7 +10,7 @@ use std::env;
use iron::prelude::*;
use juniper::{
tests::{model::Database, schema::Query},
EmptyMutation,
EmptyMutation, EmptySubscription,
};
use juniper_iron::{GraphQLHandler, GraphiQLHandler};
use logger::Logger;
@ -23,8 +23,12 @@ fn context_factory(_: &mut Request) -> IronResult<Database> {
fn main() {
let mut mount = Mount::new();
let graphql_endpoint =
GraphQLHandler::new(context_factory, Query, EmptyMutation::<Database>::new());
let graphql_endpoint = GraphQLHandler::new(
context_factory,
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
let graphiql_endpoint = GraphiQLHandler::new("/graphql");
mount.mount("/", graphiql_endpoint);

View file

@ -29,7 +29,7 @@ extern crate iron;
use iron::prelude::*;
use juniper_iron::GraphQLHandler;
use juniper::{Context, EmptyMutation};
use juniper::{Context, EmptyMutation, EmptySubscription};
# use juniper::FieldResult;
#
@ -84,7 +84,11 @@ fn main() {
// and the mutation object. If we don't have any mutations to expose, we
// can use the empty tuple () to indicate absence.
let graphql_endpoint = GraphQLHandler::new(
context_factory, QueryRoot, EmptyMutation::<Database>::new());
context_factory,
QueryRoot,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
// Start serving the schema at the root on port 8080.
Iron::new(graphql_endpoint).http("localhost:8080").unwrap();
@ -146,14 +150,15 @@ impl<S> GraphQLBatchRequest<S>
where
S: ScalarValue,
{
pub fn execute_sync<'a, CtxT, QueryT, MutationT>(
pub fn execute_sync<'a, CtxT, QueryT, MutationT, Subscription>(
&'a self,
root_node: &'a RootNode<QueryT, MutationT, S>,
root_node: &'a RootNode<QueryT, MutationT, Subscription, S>,
context: &CtxT,
) -> GraphQLBatchResponse<'a, S>
where
QueryT: GraphQLType<S, Context = CtxT>,
MutationT: GraphQLType<S, Context = CtxT>,
Subscription: GraphQLType<S, Context = CtxT>,
{
match *self {
GraphQLBatchRequest::Single(ref request) => {
@ -193,16 +198,24 @@ where
/// this endpoint containing the field `"query"` and optionally `"variables"`.
/// The variables should be a JSON object containing the variable to value
/// mapping.
pub struct GraphQLHandler<'a, CtxFactory, Query, Mutation, CtxT, S = DefaultScalarValue>
where
pub struct GraphQLHandler<
'a,
CtxFactory,
Query,
Mutation,
Subscription,
CtxT,
S = DefaultScalarValue,
> where
S: ScalarValue,
CtxFactory: Fn(&mut Request) -> IronResult<CtxT> + Send + Sync + 'static,
CtxT: 'static,
Query: GraphQLType<S, Context = CtxT> + Send + Sync + 'static,
Mutation: GraphQLType<S, Context = CtxT> + Send + Sync + 'static,
Subscription: GraphQLType<S, Context = CtxT> + Send + Sync + 'static,
{
context_factory: CtxFactory,
root_node: RootNode<'a, Query, Mutation, S>,
root_node: RootNode<'a, Query, Mutation, Subscription, S>,
}
/// Handler that renders `GraphiQL` - a graphical query editor interface
@ -246,14 +259,15 @@ where
}
}
impl<'a, CtxFactory, Query, Mutation, CtxT, S>
GraphQLHandler<'a, CtxFactory, Query, Mutation, CtxT, S>
impl<'a, CtxFactory, Query, Mutation, Subscription, CtxT, S>
GraphQLHandler<'a, CtxFactory, Query, Mutation, Subscription, CtxT, S>
where
S: ScalarValue + 'a,
CtxFactory: Fn(&mut Request) -> IronResult<CtxT> + Send + Sync + 'static,
CtxT: 'static,
Query: GraphQLType<S, Context = CtxT, TypeInfo = ()> + Send + Sync + 'static,
Mutation: GraphQLType<S, Context = CtxT, TypeInfo = ()> + Send + Sync + 'static,
Subscription: GraphQLType<S, Context = CtxT, TypeInfo = ()> + Send + Sync + 'static,
{
/// Build a new GraphQL handler
///
@ -261,10 +275,15 @@ where
/// expected to construct a context object for the given schema. This can
/// be used to construct e.g. database connections or similar data that
/// the schema needs to execute the query.
pub fn new(context_factory: CtxFactory, query: Query, mutation: Mutation) -> Self {
pub fn new(
context_factory: CtxFactory,
query: Query,
mutation: Mutation,
subscription: Subscription,
) -> Self {
GraphQLHandler {
context_factory,
root_node: RootNode::new(query, mutation),
root_node: RootNode::new(query, mutation, subscription),
}
}
@ -336,14 +355,15 @@ impl PlaygroundHandler {
}
}
impl<'a, CtxFactory, Query, Mutation, CtxT, S> Handler
for GraphQLHandler<'a, CtxFactory, Query, Mutation, CtxT, S>
impl<'a, CtxFactory, Query, Mutation, Subscription, CtxT, S> Handler
for GraphQLHandler<'a, CtxFactory, Query, Mutation, Subscription, CtxT, S>
where
S: ScalarValue + Sync + Send + 'static,
CtxFactory: Fn(&mut Request) -> IronResult<CtxT> + Send + Sync + 'static,
CtxT: 'static,
Query: GraphQLType<S, Context = CtxT, TypeInfo = ()> + Send + Sync + 'static,
Mutation: GraphQLType<S, Context = CtxT, TypeInfo = ()> + Send + Sync + 'static,
Subscription: GraphQLType<S, Context = CtxT, TypeInfo = ()> + Send + Sync + 'static,
'a: 'static,
{
fn handle(&self, mut req: &mut Request) -> IronResult<Response> {
@ -378,7 +398,7 @@ impl Handler for PlaygroundHandler {
Ok(Response::with((
content_type,
status::Ok,
juniper::http::playground::playground_source(&self.graphql_url),
juniper::http::playground::playground_source(&self.graphql_url, None),
)))
}
}
@ -427,7 +447,7 @@ mod tests {
use juniper::{
http::tests as http_tests,
tests::{model::Database, schema::Query},
EmptyMutation,
EmptyMutation, EmptySubscription,
};
use super::GraphQLHandler;
@ -520,6 +540,7 @@ mod tests {
context_factory,
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
))
}
}

View file

@ -4,10 +4,10 @@ use rocket::{response::content, State};
use juniper::{
tests::{model::Database, schema::Query},
EmptyMutation, RootNode,
EmptyMutation, EmptySubscription, RootNode,
};
type Schema = RootNode<'static, Query, EmptyMutation<Database>>;
type Schema = RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>;
#[rocket::get("/")]
fn graphiql() -> content::Html<String> {
@ -35,7 +35,11 @@ fn post_graphql_handler(
fn main() {
rocket::ignite()
.manage(Database::new())
.manage(Schema::new(Query, EmptyMutation::<Database>::new()))
.manage(Schema::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
))
.mount(
"/",
rocket::routes![graphiql, get_graphql_handler, post_graphql_handler],

View file

@ -82,14 +82,15 @@ impl<S> GraphQLBatchRequest<S>
where
S: ScalarValue,
{
pub fn execute_sync<'a, CtxT, QueryT, MutationT>(
pub fn execute_sync<'a, CtxT, QueryT, MutationT, SubscriptionT>(
&'a self,
root_node: &'a RootNode<QueryT, MutationT, S>,
root_node: &'a RootNode<QueryT, MutationT, SubscriptionT, S>,
context: &CtxT,
) -> GraphQLBatchResponse<'a, S>
where
QueryT: GraphQLType<S, Context = CtxT>,
MutationT: GraphQLType<S, Context = CtxT>,
SubscriptionT: GraphQLType<S, Context = CtxT>,
{
match *self {
GraphQLBatchRequest::Single(ref request) => {
@ -150,6 +151,7 @@ pub fn graphiql_source(graphql_endpoint_url: &str) -> content::Html<String> {
pub fn playground_source(graphql_endpoint_url: &str) -> content::Html<String> {
content::Html(juniper::http::playground::playground_source(
graphql_endpoint_url,
None,
))
}
@ -158,14 +160,15 @@ where
S: ScalarValue,
{
/// Execute an incoming GraphQL query
pub fn execute_sync<CtxT, QueryT, MutationT>(
pub fn execute_sync<CtxT, QueryT, MutationT, SubscriptionT>(
&self,
root_node: &RootNode<QueryT, MutationT, S>,
root_node: &RootNode<QueryT, MutationT, SubscriptionT, S>,
context: &CtxT,
) -> GraphQLResponse
where
QueryT: GraphQLType<S, Context = CtxT>,
MutationT: GraphQLType<S, Context = CtxT>,
SubscriptionT: GraphQLType<S, Context = CtxT>,
{
let response = self.0.execute_sync(root_node, context);
let status = if response.is_ok() {
@ -205,9 +208,9 @@ impl GraphQLResponse {
/// #
/// # use juniper::tests::schema::Query;
/// # use juniper::tests::model::Database;
/// # use juniper::{EmptyMutation, FieldError, RootNode, Value};
/// # use juniper::{EmptyMutation, EmptySubscription, FieldError, RootNode, Value};
/// #
/// # type Schema = RootNode<'static, Query, EmptyMutation<Database>>;
/// # type Schema = RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>;
/// #
/// #[rocket::get("/graphql?<request..>")]
/// fn get_graphql_handler(
@ -489,10 +492,10 @@ mod tests {
use juniper::{
http::tests as http_tests,
tests::{model::Database, schema::Query},
EmptyMutation, RootNode,
EmptyMutation, EmptySubscription, RootNode,
};
type Schema = RootNode<'static, Query, EmptyMutation<Database>>;
type Schema = RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>;
#[get("/?<request..>")]
fn get_graphql_handler(
@ -567,9 +570,11 @@ mod tests {
}
fn make_rocket_without_routes() -> Rocket {
rocket::ignite()
.manage(Database::new())
.manage(Schema::new(Query, EmptyMutation::<Database>::new()))
rocket::ignite().manage(Database::new()).manage(Schema::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
))
}
fn make_test_response(request: &LocalRequest) -> http_tests::TestResponse {

4
juniper_subscriptions/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/target
/examples/**/target/**/*
**/*.rs.bk
Cargo.lock

View file

@ -0,0 +1,3 @@
# master
- Initial Release

View file

@ -0,0 +1,18 @@
[package]
name = "juniper_subscriptions"
version = "0.14.2"
authors = ["nWacky <gosha.evtushenko@gmail.com>"]
description = "Juniper SubscriptionCoordinator and SubscriptionConnection implementations"
license = "BSD-2-Clause"
documentation = "https://docs.rs/juniper_subscriptions"
repository = "https://github.com/graphql-rust/juniper"
edition = "2018"
[dependencies]
futures = { version = "=0.3.1", features = ["compat"] }
juniper = { version = "0.14.2", path = "../juniper", default-features = false }
[dev-dependencies]
serde_json = "1.0"
tokio = { version = "0.2", features = ["rt-core", "macros"] }

View file

@ -0,0 +1,25 @@
BSD 2-Clause License
Copyright (c) 2018, Tom Houlé
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,21 @@
[env]
CARGO_MAKE_CARGO_ALL_FEATURES = ""
[tasks.build-verbose]
condition = { rust_version = { min = "1.29.0" } }
[tasks.build-verbose.windows]
condition = { rust_version = { min = "1.29.0" }, env = { "TARGET" = "x86_64-pc-windows-msvc" } }
[tasks.test-verbose]
condition = { rust_version = { min = "1.29.0" } }
[tasks.test-verbose.windows]
condition = { rust_version = { min = "1.29.0" }, env = { "TARGET" = "x86_64-pc-windows-msvc" } }
[tasks.ci-coverage-flow]
condition = { rust_version = { min = "1.29.0" } }
[tasks.ci-coverage-flow.windows]
disabled = true

View file

@ -0,0 +1,37 @@
# juniper_subscriptions
This repository contains [SubscriptionCoordinator][SubscriptionCoordinator] and
[SubscriptionConnection][SubscriptionConnection] implementations for
[Juniper][Juniper], a [GraphQL][GraphQL] library for Rust.
## Documentation
For this crate's documentation, check out [API documentation][documentation].
For `SubscriptionCoordinator` and `SubscriptionConnection` documentation, check
out [Juniper][Juniper].
## Examples
Check [examples/warp_subscriptions][example] for example code of a working
[warp][warp] server with GraphQL subscription handlers.
## Links
* [Juniper][Juniper]
* [API Reference][documentation]
* [warp][warp]
## License
This project is under the BSD-2 license.
Check the LICENSE file for details.
[warp]: https://github.com/seanmonstar/warp
[Juniper]: https://github.com/graphql-rust/juniper
[SubscriptionCoordinator]: https://docs.rs/juniper/latest/juniper/trait.SubscriptionCoordinator.html
[SubscriptionConnection]: https://docs.rs/juniper/latest/juniper/trait.SubscriptionConnection.html
[GraphQL]: http://graphql.org
[documentation]: https://docs.rs/juniper_subscriptions
[example]: https://github.com/graphql-rust/juniper/blob/master/examples/warp_subscriptions/src/main.rs

View file

@ -0,0 +1,8 @@
no-dev-version = true
pre-release-commit-message = "Release {{crate_name}} {{version}}"
pro-release-commit-message = "Bump {{crate_name}} version to {{next_version}}"
tag-message = "Release {{crate_name}} {{version}}"
upload-doc = false
pre-release-replacements = [
{file="src/lib.rs", search="docs.rs/juniper_subscriptions/[a-z0-9\\.-]+", replace="docs.rs/juniper_subscriptions/{{version}}"},
]

View file

@ -0,0 +1,446 @@
//! This crate supplies [`SubscriptionCoordinator`] and
//! [`SubscriptionConnection`] implementations for the
//! [juniper](https://github.com/graphql-rust/juniper) crate.
//!
//! You need both this and `juniper` crate.
//!
//! [`SubscriptionCoordinator`]: juniper::SubscriptionCoordinator
//! [`SubscriptionConnection`]: juniper::SubscriptionConnection
#![deny(missing_docs)]
#![deny(warnings)]
#![doc(html_root_url = "https://docs.rs/juniper_subscriptions/0.14.2")]
use std::{borrow::BorrowMut as _, iter::FromIterator, pin::Pin};
use futures::{task::Poll, Stream};
use juniper::{
http::{GraphQLRequest, GraphQLResponse},
BoxFuture, ExecutionError, GraphQLError, GraphQLSubscriptionType, GraphQLTypeAsync, Object,
ScalarValue, SubscriptionConnection, SubscriptionCoordinator, Value, ValuesStream,
};
/// Simple [`SubscriptionCoordinator`] implementation:
/// - contains the schema
/// - handles subscription start
pub struct Coordinator<'a, QueryT, MutationT, SubscriptionT, CtxT, S>
where
S: ScalarValue + Send + Sync + 'static,
QueryT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
QueryT::TypeInfo: Send + Sync,
MutationT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
MutationT::TypeInfo: Send + Sync,
SubscriptionT: GraphQLSubscriptionType<S, Context = CtxT> + Send + Sync,
SubscriptionT::TypeInfo: Send + Sync,
CtxT: Send + Sync,
{
root_node: juniper::RootNode<'a, QueryT, MutationT, SubscriptionT, S>,
}
impl<'a, QueryT, MutationT, SubscriptionT, CtxT, S>
Coordinator<'a, QueryT, MutationT, SubscriptionT, CtxT, S>
where
S: ScalarValue + Send + Sync + 'static,
QueryT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
QueryT::TypeInfo: Send + Sync,
MutationT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
MutationT::TypeInfo: Send + Sync,
SubscriptionT: GraphQLSubscriptionType<S, Context = CtxT> + Send + Sync,
SubscriptionT::TypeInfo: Send + Sync,
CtxT: Send + Sync,
{
/// Builds new [`Coordinator`] with specified `root_node`
pub fn new(root_node: juniper::RootNode<'a, QueryT, MutationT, SubscriptionT, S>) -> Self {
Self { root_node }
}
}
impl<'a, QueryT, MutationT, SubscriptionT, CtxT, S> SubscriptionCoordinator<'a, CtxT, S>
for Coordinator<'a, QueryT, MutationT, SubscriptionT, CtxT, S>
where
S: ScalarValue + Send + Sync + 'a,
QueryT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
QueryT::TypeInfo: Send + Sync,
MutationT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
MutationT::TypeInfo: Send + Sync,
SubscriptionT: GraphQLSubscriptionType<S, Context = CtxT> + Send + Sync,
SubscriptionT::TypeInfo: Send + Sync,
CtxT: Send + Sync,
{
type Connection = Connection<'a, S>;
type Error = GraphQLError<'a>;
fn subscribe(
&'a self,
req: &'a GraphQLRequest<S>,
context: &'a CtxT,
) -> BoxFuture<'a, Result<Self::Connection, Self::Error>> {
let rn = &self.root_node;
Box::pin(async move {
let (stream, errors) = juniper::http::resolve_into_stream(req, rn, context).await?;
Ok(Connection::from_stream(stream, errors))
})
}
}
/// Simple [`SubscriptionConnection`] implementation.
///
/// Resolves `Value<ValuesStream>` into `Stream<Item = GraphQLResponse>` using the following
/// logic:
///
/// [`Value::Null`] - returns [`Value::Null`] once
/// [`Value::Scalar`] - returns `Ok` value or [`Value::Null`] and errors vector
/// [`Value::List`] - resolves each stream from the list using current logic and returns
/// values in the order received
/// [`Value::Object`] - waits while each field of the [`Object`] is returned, then yields the whole object
/// `Value::Object<Value::Object<_>>` - returns [`Value::Null`] if [`Value::Object`] consists of sub-objects
pub struct Connection<'a, S> {
stream: Pin<Box<dyn futures::Stream<Item = GraphQLResponse<'a, S>> + Send + 'a>>,
}
impl<'a, S> Connection<'a, S>
where
S: ScalarValue + Send + Sync + 'a,
{
/// Creates new [`Connection`] from values stream and errors
pub fn from_stream(stream: Value<ValuesStream<'a, S>>, errors: Vec<ExecutionError<S>>) -> Self {
Self {
stream: whole_responses_stream(stream, errors),
}
}
}
impl<'a, S> SubscriptionConnection<'a, S> for Connection<'a, S> where
S: ScalarValue + Send + Sync + 'a
{
}
impl<'a, S> futures::Stream for Connection<'a, S>
where
S: ScalarValue + Send + Sync + 'a,
{
type Item = GraphQLResponse<'a, S>;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut futures::task::Context<'_>,
) -> Poll<Option<Self::Item>> {
// this is safe as stream is only mutated here and is not moved anywhere
let Connection { stream } = unsafe { self.get_unchecked_mut() };
let stream = unsafe { Pin::new_unchecked(stream) };
stream.poll_next(cx)
}
}
/// Creates [`futures::Stream`] that yields [`GraphQLResponse`]s depending on the given [`Value`]:
///
/// [`Value::Null`] - returns [`Value::Null`] once
/// [`Value::Scalar`] - returns `Ok` value or [`Value::Null`] and errors vector
/// [`Value::List`] - resolves each stream from the list using current logic and returns
/// values in the order received
/// [`Value::Object`] - waits while each field of the [`Object`] is returned, then yields the whole object
/// `Value::Object<Value::Object<_>>` - returns [`Value::Null`] if [`Value::Object`] consists of sub-objects
fn whole_responses_stream<'a, S>(
stream: Value<ValuesStream<'a, S>>,
errors: Vec<ExecutionError<S>>,
) -> Pin<Box<dyn futures::Stream<Item = GraphQLResponse<'a, S>> + Send + 'a>>
where
S: ScalarValue + Send + Sync + 'a,
{
use futures::stream::{self, StreamExt as _};
if !errors.is_empty() {
return Box::pin(stream::once(async move {
GraphQLResponse::from_result(Ok((Value::Null, errors)))
}));
}
match stream {
Value::Null => Box::pin(stream::once(async move {
GraphQLResponse::from_result(Ok((Value::Null, vec![])))
})),
Value::Scalar(s) => Box::pin(s.map(|res| match res {
Ok(val) => GraphQLResponse::from_result(Ok((val, vec![]))),
Err(err) => GraphQLResponse::from_result(Ok((Value::Null, vec![err]))),
})),
Value::List(list) => {
let mut streams = vec![];
for s in list.into_iter() {
streams.push(whole_responses_stream(s, vec![]));
}
Box::pin(stream::select_all(streams))
}
Value::Object(mut object) => {
let obj_len = object.field_count();
if obj_len == 0 {
return Box::pin(stream::once(async move {
GraphQLResponse::from_result(Ok((Value::Null, vec![])))
}));
}
let mut filled_count = 0;
let mut ready_vec = Vec::with_capacity(obj_len);
for _ in 0..obj_len {
ready_vec.push(None);
}
let stream = futures::stream::poll_fn(
move |mut ctx| -> Poll<Option<GraphQLResponse<'static, S>>> {
let mut obj_iterator = object.iter_mut();
// Due to having to modify `ready_vec` contents (by-move pattern)
// and only being able to iterate over `object`'s mutable references (by-ref pattern)
// `ready_vec` and `object` cannot be iterated simultaneously.
// TODO: iterate over i and (ref field_name, ref val) once
// [this RFC](https://github.com/rust-lang/rust/issues/68354)
// is implemented
for i in 0..obj_len {
let (field_name, val) = match obj_iterator.next() {
Some(v) => v,
None => break,
};
let ready = ready_vec[i].borrow_mut();
if ready.is_some() {
continue;
}
match val {
Value::Scalar(stream) => {
match Pin::new(stream).poll_next(&mut ctx) {
Poll::Ready(None) => return Poll::Ready(None),
Poll::Ready(Some(value)) => {
*ready = Some((field_name.clone(), value));
filled_count += 1;
}
Poll::Pending => { /* check back later */ }
}
}
_ => {
// For now only `Object<Value::Scalar>` is supported
*ready = Some((field_name.clone(), Ok(Value::Null)));
filled_count += 1;
}
}
}
if filled_count == obj_len {
filled_count = 0;
let new_vec = (0..obj_len).map(|_| None).collect::<Vec<_>>();
let ready_vec = std::mem::replace(&mut ready_vec, new_vec);
let ready_vec_iterator = ready_vec.into_iter().map(|el| {
let (name, val) = el.unwrap();
if let Ok(value) = val {
(name, value)
} else {
(name, Value::Null)
}
});
let obj = Object::from_iter(ready_vec_iterator);
return Poll::Ready(Some(GraphQLResponse::from_result(Ok((
Value::Object(obj),
vec![],
)))));
} else {
return Poll::Pending;
}
},
);
Box::pin(stream)
}
}
}
#[cfg(test)]
mod whole_responses_stream {
use super::*;
use futures::{stream, StreamExt as _};
use juniper::{DefaultScalarValue, ExecutionError, FieldError};
#[tokio::test]
async fn with_error() {
let expected = vec![GraphQLResponse::<DefaultScalarValue>::error(
FieldError::new("field error", Value::Null),
)];
let expected = serde_json::to_string(&expected).unwrap();
let result = whole_responses_stream::<DefaultScalarValue>(
Value::Null,
vec![ExecutionError::at_origin(FieldError::new(
"field error",
Value::Null,
))],
)
.collect::<Vec<_>>()
.await;
let result = serde_json::to_string(&result).unwrap();
assert_eq!(result, expected);
}
#[tokio::test]
async fn value_null() {
let expected = vec![GraphQLResponse::<DefaultScalarValue>::from_result(Ok((
Value::Null,
vec![],
)))];
let expected = serde_json::to_string(&expected).unwrap();
let result = whole_responses_stream::<DefaultScalarValue>(Value::Null, vec![])
.collect::<Vec<_>>()
.await;
let result = serde_json::to_string(&result).unwrap();
assert_eq!(result, expected);
}
type PollResult = Result<Value<DefaultScalarValue>, ExecutionError<DefaultScalarValue>>;
#[tokio::test]
async fn value_scalar() {
let expected = vec![
GraphQLResponse::from_result(Ok((
Value::Scalar(DefaultScalarValue::Int(1i32)),
vec![],
))),
GraphQLResponse::from_result(Ok((
Value::Scalar(DefaultScalarValue::Int(2i32)),
vec![],
))),
GraphQLResponse::from_result(Ok((
Value::Scalar(DefaultScalarValue::Int(3i32)),
vec![],
))),
GraphQLResponse::from_result(Ok((
Value::Scalar(DefaultScalarValue::Int(4i32)),
vec![],
))),
GraphQLResponse::from_result(Ok((
Value::Scalar(DefaultScalarValue::Int(5i32)),
vec![],
))),
];
let expected = serde_json::to_string(&expected).unwrap();
let mut counter = 0;
let stream = stream::poll_fn(move |_| -> Poll<Option<PollResult>> {
if counter == 5 {
return Poll::Ready(None);
}
counter += 1;
Poll::Ready(Some(Ok(Value::Scalar(DefaultScalarValue::Int(counter)))))
});
let result =
whole_responses_stream::<DefaultScalarValue>(Value::Scalar(Box::pin(stream)), vec![])
.collect::<Vec<_>>()
.await;
let result = serde_json::to_string(&result).unwrap();
assert_eq!(result, expected);
}
#[tokio::test]
async fn value_list() {
let expected = vec![
GraphQLResponse::from_result(Ok((
Value::Scalar(DefaultScalarValue::Int(1i32)),
vec![],
))),
GraphQLResponse::from_result(Ok((
Value::Scalar(DefaultScalarValue::Int(2i32)),
vec![],
))),
GraphQLResponse::from_result(Ok((Value::Null, vec![]))),
GraphQLResponse::from_result(Ok((
Value::Scalar(DefaultScalarValue::Int(4i32)),
vec![],
))),
];
let expected = serde_json::to_string(&expected).unwrap();
let streams: Vec<Value<ValuesStream>> = vec![
Value::Scalar(Box::pin(stream::once(async {
PollResult::Ok(Value::Scalar(DefaultScalarValue::Int(1i32)))
}))),
Value::Scalar(Box::pin(stream::once(async {
PollResult::Ok(Value::Scalar(DefaultScalarValue::Int(2i32)))
}))),
Value::Null,
Value::Scalar(Box::pin(stream::once(async {
PollResult::Ok(Value::Scalar(DefaultScalarValue::Int(4i32)))
}))),
];
let result = whole_responses_stream::<DefaultScalarValue>(Value::List(streams), vec![])
.collect::<Vec<_>>()
.await;
let result = serde_json::to_string(&result).unwrap();
assert_eq!(result, expected);
}
#[tokio::test]
async fn value_object() {
let expected = vec![
GraphQLResponse::from_result(Ok((
Value::Object(Object::from_iter(
vec![
("one", Value::Scalar(DefaultScalarValue::Int(1i32))),
("two", Value::Scalar(DefaultScalarValue::Int(1i32))),
]
.into_iter(),
)),
vec![],
))),
GraphQLResponse::from_result(Ok((
Value::Object(Object::from_iter(
vec![
("one", Value::Scalar(DefaultScalarValue::Int(2i32))),
("two", Value::Scalar(DefaultScalarValue::Int(2i32))),
]
.into_iter(),
)),
vec![],
))),
];
let expected = serde_json::to_string(&expected).unwrap();
let mut counter = 0;
let big_stream = stream::poll_fn(move |_| -> Poll<Option<PollResult>> {
if counter == 2 {
return Poll::Ready(None);
}
counter += 1;
Poll::Ready(Some(Ok(Value::Scalar(DefaultScalarValue::Int(counter)))))
});
let mut counter = 0;
let small_stream = stream::poll_fn(move |_| -> Poll<Option<PollResult>> {
if counter == 2 {
return Poll::Ready(None);
}
counter += 1;
Poll::Ready(Some(Ok(Value::Scalar(DefaultScalarValue::Int(counter)))))
});
let vals: Vec<(&str, Value<ValuesStream>)> = vec![
("one", Value::Scalar(Box::pin(big_stream))),
("two", Value::Scalar(Box::pin(small_stream))),
];
let result = whole_responses_stream::<DefaultScalarValue>(
Value::Object(Object::from_iter(vals.into_iter())),
vec![],
)
.collect::<Vec<_>>()
.await;
let result = serde_json::to_string(&result).unwrap();
assert_eq!(result, expected);
}
}

View file

@ -2,6 +2,12 @@
- Compatibility with the latest `juniper`.
## Breaking Changes
- Update `playground_filter` to support subscription endpoint URLs
- Update `warp` to 0.2
- Rename synchronous `execute` to `execute_sync`, add asynchronous `execute`
# [[0.5.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_warp-0.5.2)
- Compatibility with the latest `juniper`.

View file

@ -8,21 +8,23 @@ documentation = "https://docs.rs/juniper_warp"
repository = "https://github.com/graphql-rust/juniper"
edition = "2018"
[features]
subscriptions = ["juniper_subscriptions"]
[dependencies]
warp = "0.1.8"
warp = "0.2"
futures = { version = "0.3.1", features = ["compat"] }
juniper = { version = "0.14.2", path = "../juniper", default-features = false }
juniper_subscriptions = { path = "../juniper_subscriptions", optional = true}
tokio = { version = "0.2", features = ["rt-core", "blocking"] }
serde_json = "1.0.24"
serde_derive = "1.0.75"
failure = "0.1.2"
# TODO: rebase juniper_warp to futures 3 once warp supports it
futures = "0.1.29"
failure = "0.1.7"
serde = "1.0.75"
tokio-threadpool = "0.1.7"
futures03 = { version = "0.3.1", optional = true, package = "futures", features = ["compat"] }
[dev-dependencies]
juniper = { version = "0.14.2", path = "../juniper", features = ["expose-test-schema", "serde_json"] }
env_logger = "0.5.11"
log = "0.4.3"
percent-encoding = "1.0"
tokio = { version = "0.2", features = ["rt-core", "macros", "blocking"] }

View file

@ -4,17 +4,22 @@ extern crate log;
use juniper::{
tests::{model::Database, schema::Query},
EmptyMutation, RootNode,
EmptyMutation, EmptySubscription, RootNode,
};
use warp::{http::Response, Filter};
type Schema = RootNode<'static, Query, EmptyMutation<Database>>;
type Schema = RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>;
fn schema() -> Schema {
Schema::new(Query, EmptyMutation::<Database>::new())
Schema::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
)
}
fn main() {
#[tokio::main]
async fn main() {
::std::env::set_var("RUST_LOG", "warp_server");
env_logger::init();
@ -34,12 +39,13 @@ fn main() {
let graphql_filter = juniper_warp::make_graphql_filter(schema(), state.boxed());
warp::serve(
warp::get2()
warp::get()
.and(warp::path("graphiql"))
.and(juniper_warp::graphiql_filter("/graphql"))
.or(homepage)
.or(warp::path("graphql").and(graphql_filter))
.with(log),
)
.run(([127, 0, 0, 1], 8080));
.run(([127, 0, 0, 1], 8080))
.await
}

View file

@ -40,14 +40,13 @@ Check the LICENSE file for details.
#![deny(warnings)]
#![doc(html_root_url = "https://docs.rs/juniper_warp/0.2.0")]
use futures::{future::poll_fn, Future};
use serde::Deserialize;
use std::sync::Arc;
use warp::{filters::BoxedFilter, Filter};
use futures03::future::{FutureExt, TryFutureExt};
use std::{pin::Pin, sync::Arc};
use futures::{Future, FutureExt as _, TryFutureExt};
use juniper::{DefaultScalarValue, InputValue, ScalarValue};
use serde::Deserialize;
use tokio::task;
use warp::{filters::BoxedFilter, Filter};
#[derive(Debug, serde_derive::Deserialize, PartialEq)]
#[serde(untagged)]
@ -64,20 +63,23 @@ impl<S> GraphQLBatchRequest<S>
where
S: ScalarValue,
{
pub fn execute<'a, CtxT, QueryT, MutationT>(
pub fn execute_sync<'a, CtxT, QueryT, MutationT, SubscriptionT>(
&'a self,
root_node: &'a juniper::RootNode<QueryT, MutationT, S>,
root_node: &'a juniper::RootNode<QueryT, MutationT, SubscriptionT, S>,
context: &CtxT,
) -> GraphQLBatchResponse<'a, S>
where
QueryT: juniper::GraphQLType<S, Context = CtxT>,
MutationT: juniper::GraphQLType<S, Context = CtxT>,
SubscriptionT: juniper::GraphQLType<S, Context = CtxT>,
SubscriptionT::TypeInfo: Send + Sync,
CtxT: Send + Sync,
{
match *self {
GraphQLBatchRequest::Single(ref request) => {
match self {
&GraphQLBatchRequest::Single(ref request) => {
GraphQLBatchResponse::Single(request.execute_sync(root_node, context))
}
GraphQLBatchRequest::Batch(ref requests) => GraphQLBatchResponse::Batch(
&GraphQLBatchRequest::Batch(ref requests) => GraphQLBatchResponse::Batch(
requests
.iter()
.map(|request| request.execute_sync(root_node, context))
@ -86,9 +88,9 @@ where
}
}
pub async fn execute<'a, CtxT, QueryT, MutationT>(
pub async fn execute<'a, CtxT, QueryT, MutationT, SubscriptionT>(
&'a self,
root_node: &'a juniper::RootNode<'a, QueryT, MutationT, S>,
root_node: &'a juniper::RootNode<'a, QueryT, MutationT, SubscriptionT, S>,
context: &'a CtxT,
) -> GraphQLBatchResponse<'a, S>
where
@ -96,6 +98,8 @@ where
QueryT::TypeInfo: Send + Sync,
MutationT: juniper::GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
MutationT::TypeInfo: Send + Sync,
SubscriptionT: juniper::GraphQLSubscriptionType<S, Context = CtxT> + Send + Sync,
SubscriptionT::TypeInfo: Send + Sync,
CtxT: Send + Sync,
S: Send + Sync,
{
@ -109,7 +113,7 @@ where
.iter()
.map(|request| request.execute(root_node, context))
.collect::<Vec<_>>();
let responses = futures03::future::join_all(futures).await;
let responses = futures::future::join_all(futures).await;
GraphQLBatchResponse::Batch(responses)
}
@ -139,7 +143,7 @@ where
}
}
/// Make a filter for graphql endpoint.
/// Make a filter for graphql queries/mutations.
///
/// The `schema` argument is your juniper schema.
///
@ -156,7 +160,7 @@ where
/// #
/// # use std::sync::Arc;
/// # use warp::Filter;
/// # use juniper::{EmptyMutation, RootNode};
/// # use juniper::{EmptyMutation, EmptySubscription, RootNode};
/// # use juniper_warp::make_graphql_filter;
/// #
/// type UserId = String;
@ -179,7 +183,7 @@ where
/// }
/// }
///
/// let schema = RootNode::new(QueryRoot, EmptyMutation::new());
/// let schema = RootNode::new(QueryRoot, EmptyMutation::new(), EmptySubscription::new());
///
/// let app_state = Arc::new(AppState(vec![3, 4, 5]));
/// let app_state = warp::any().map(move || app_state.clone());
@ -196,82 +200,11 @@ where
/// let graphql_filter = make_graphql_filter(schema, context_extractor);
///
/// let graphql_endpoint = warp::path("graphql")
/// .and(warp::post2())
/// .and(warp::post())
/// .and(graphql_filter);
/// ```
pub fn make_graphql_filter<Query, Mutation, Context, S>(
schema: juniper::RootNode<'static, Query, Mutation, S>,
context_extractor: BoxedFilter<(Context,)>,
) -> BoxedFilter<(warp::http::Response<Vec<u8>>,)>
where
S: ScalarValue + Send + Sync + 'static,
Context: Send + 'static,
Query: juniper::GraphQLType<S, Context = Context, TypeInfo = ()> + Send + Sync + 'static,
Mutation: juniper::GraphQLType<S, Context = Context, TypeInfo = ()> + Send + Sync + 'static,
{
let schema = Arc::new(schema);
let post_schema = schema.clone();
let handle_post_request =
move |context: Context, request: GraphQLBatchRequest<S>| -> Response {
let schema = post_schema.clone();
Box::new(
poll_fn(move || {
tokio_threadpool::blocking(|| {
let response = request.execute_sync(&schema, &context);
Ok((serde_json::to_vec(&response)?, response.is_ok()))
})
})
.and_then(|result| ::futures::future::done(Ok(build_response(result))))
.map_err(warp::reject::custom),
)
};
let post_filter = warp::post2()
.and(context_extractor.clone())
.and(warp::body::json())
.and_then(handle_post_request);
let handle_get_request = move |context: Context,
mut request: std::collections::HashMap<String, String>|
-> Response {
let schema = schema.clone();
Box::new(
poll_fn(move || {
tokio_threadpool::blocking(|| {
let variables = match request.remove("variables") {
None => None,
Some(vs) => serde_json::from_str(&vs)?,
};
let graphql_request = juniper::http::GraphQLRequest::new(
request.remove("query").ok_or_else(|| {
failure::format_err!("Missing GraphQL query string in query parameters")
})?,
request.get("operation_name").map(|s| s.to_owned()),
variables,
);
let response = graphql_request.execute_sync(&schema, &context);
Ok((serde_json::to_vec(&response)?, response.is_ok()))
})
})
.and_then(|result| ::futures::future::done(Ok(build_response(result))))
.map_err(warp::reject::custom),
)
};
let get_filter = warp::get2()
.and(context_extractor)
.and(warp::filters::query::query())
.and_then(handle_get_request);
get_filter.or(post_filter).unify().boxed()
}
/// FIXME: docs
pub fn make_graphql_filter_async<Query, Mutation, Context, S>(
schema: juniper::RootNode<'static, Query, Mutation, S>,
pub fn make_graphql_filter<Query, Mutation, Subscription, Context, S>(
schema: juniper::RootNode<'static, Query, Mutation, Subscription, S>,
context_extractor: BoxedFilter<(Context,)>,
) -> BoxedFilter<(warp::http::Response<Vec<u8>>,)>
where
@ -281,6 +214,75 @@ where
Query::TypeInfo: Send + Sync,
Mutation: juniper::GraphQLTypeAsync<S, Context = Context> + Send + Sync + 'static,
Mutation::TypeInfo: Send + Sync,
Subscription: juniper::GraphQLSubscriptionType<S, Context = Context> + Send + Sync + 'static,
Subscription::TypeInfo: Send + Sync,
{
let schema = Arc::new(schema);
let post_schema = schema.clone();
let handle_post_request = move |context: Context, request: GraphQLBatchRequest<S>| {
let schema = post_schema.clone();
Box::pin(async move {
let res = request.execute(&schema, &context).await;
Ok::<_, warp::Rejection>(build_response(
serde_json::to_vec(&res)
.map(|json| (json, res.is_ok()))
.map_err(Into::into),
))
})
};
let post_filter = warp::post()
.and(context_extractor.clone())
.and(warp::body::json())
.and_then(handle_post_request);
let handle_get_request =
move |context: Context, mut request: std::collections::HashMap<String, String>| {
let schema = schema.clone();
async move {
let variables = match request.remove("variables") {
None => None,
Some(vs) => serde_json::from_str(&vs)?,
};
let graphql_request = juniper::http::GraphQLRequest::new(
request.remove("query").ok_or_else(|| {
failure::format_err!("Missing GraphQL query string in query parameters")
})?,
request.get("operation_name").map(|s| s.to_owned()),
variables,
);
let response = graphql_request.execute(&schema, &context).await;
Ok((serde_json::to_vec(&response)?, response.is_ok()))
}
.then(|result| async move { Ok::<_, warp::Rejection>(build_response(result)) })
};
let get_filter = warp::get()
.and(context_extractor.clone())
.and(warp::filters::query::query())
.and_then(handle_get_request);
get_filter.or(post_filter).unify().boxed()
}
/// Make a synchronous filter for graphql endpoint.
pub fn make_graphql_filter_sync<Query, Mutation, Subscription, Context, S>(
schema: juniper::RootNode<'static, Query, Mutation, Subscription, S>,
context_extractor: BoxedFilter<(Context,)>,
) -> BoxedFilter<(warp::http::Response<Vec<u8>>,)>
where
S: ScalarValue + Send + Sync + 'static,
Context: Send + Sync + 'static,
Query: juniper::GraphQLType<S, Context = Context, TypeInfo = ()> + Send + Sync + 'static,
Mutation: juniper::GraphQLType<S, Context = Context, TypeInfo = ()> + Send + Sync + 'static,
Subscription: juniper::GraphQLType<S, Context = Context, TypeInfo = ()> + Send + Sync + 'static,
{
let schema = Arc::new(schema);
let post_schema = schema.clone();
@ -289,19 +291,21 @@ where
move |context: Context, request: GraphQLBatchRequest<S>| -> Response {
let schema = post_schema.clone();
let f = async move {
let res = request.execute(&schema, &context).await;
Box::pin(
async move {
let result = task::spawn_blocking(move || {
let response = request.execute_sync(&schema, &context);
Ok((serde_json::to_vec(&response)?, response.is_ok()))
})
.await?;
match serde_json::to_vec(&res) {
Ok(json) => Ok(build_response(Ok((json, res.is_ok())))),
Err(e) => Err(warp::reject::custom(e)),
Ok(build_response(result))
}
};
Box::new(f.boxed().compat())
.map_err(|e: task::JoinError| warp::reject::custom(JoinError(e))),
)
};
let post_filter = warp::post2()
let post_filter = warp::post()
.and(context_extractor.clone())
.and(warp::body::json())
.and_then(handle_post_request);
@ -310,9 +314,10 @@ where
mut request: std::collections::HashMap<String, String>|
-> Response {
let schema = schema.clone();
Box::new(
poll_fn(move || {
tokio_threadpool::blocking(|| {
Box::pin(
async move {
let result = task::spawn_blocking(move || {
let variables = match request.remove("variables") {
None => None,
Some(vs) => serde_json::from_str(&vs)?,
@ -329,13 +334,15 @@ where
let response = graphql_request.execute_sync(&schema, &context);
Ok((serde_json::to_vec(&response)?, response.is_ok()))
})
})
.and_then(|result| ::futures::future::done(Ok(build_response(result))))
.map_err(warp::reject::custom),
.await?;
Ok(build_response(result))
}
.map_err(|e: task::JoinError| warp::reject::custom(JoinError(e))),
)
};
let get_filter = warp::get2()
let get_filter = warp::get()
.and(context_extractor.clone())
.and(warp::filters::query::query())
.and_then(handle_get_request);
@ -343,6 +350,20 @@ where
get_filter.or(post_filter).unify().boxed()
}
/// Error raised by `tokio_threadpool` if the thread pool
/// has been shutdown
///
/// Wrapper type is needed as inner type does not implement `warp::reject::Reject`
pub struct JoinError(task::JoinError);
impl warp::reject::Reject for JoinError {}
impl std::fmt::Debug for JoinError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "JoinError({:?})", self.0)
}
}
fn build_response(
response: Result<(Vec<u8>, bool), failure::Error>,
) -> warp::http::Response<Vec<u8>> {
@ -359,8 +380,9 @@ fn build_response(
}
}
type Response =
Box<dyn Future<Item = warp::http::Response<Vec<u8>>, Error = warp::reject::Rejection> + Send>;
type Response = Pin<
Box<dyn Future<Output = Result<warp::http::Response<Vec<u8>>, warp::reject::Rejection>> + Send>,
>;
/// Create a filter that replies with an HTML page containing GraphiQL. This does not handle routing, so you can mount it on any endpoint.
///
@ -393,19 +415,216 @@ fn graphiql_response(graphql_endpoint_url: &'static str) -> warp::http::Response
/// Create a filter that replies with an HTML page containing GraphQL Playground. This does not handle routing, so you can mount it on any endpoint.
pub fn playground_filter(
graphql_endpoint_url: &'static str,
subscriptions_endpoint_url: Option<&'static str>,
) -> warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)> {
warp::any()
.map(move || playground_response(graphql_endpoint_url))
.map(move || playground_response(graphql_endpoint_url, subscriptions_endpoint_url))
.boxed()
}
fn playground_response(graphql_endpoint_url: &'static str) -> warp::http::Response<Vec<u8>> {
fn playground_response(
graphql_endpoint_url: &'static str,
subscriptions_endpoint_url: Option<&'static str>,
) -> warp::http::Response<Vec<u8>> {
warp::http::Response::builder()
.header("content-type", "text/html;charset=utf-8")
.body(juniper::http::playground::playground_source(graphql_endpoint_url).into_bytes())
.body(
juniper::http::playground::playground_source(
graphql_endpoint_url,
subscriptions_endpoint_url,
)
.into_bytes(),
)
.expect("response is valid")
}
/// `juniper_warp` subscriptions handler implementation.
/// Cannot be merged to `juniper_warp` yet as GraphQL over WS[1]
/// is not fully supported in current implementation.
///
/// [1]: https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md
#[cfg(feature = "subscriptions")]
pub mod subscriptions {
use std::{
collections::HashMap,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use futures::{channel::mpsc, stream::StreamExt as _, Future};
use juniper::{http::GraphQLRequest, InputValue, ScalarValue, SubscriptionCoordinator as _};
use juniper_subscriptions::Coordinator;
use serde::{Deserialize, Serialize};
use warp::ws::Message;
/// Listen to incoming messages and do one of the following:
/// - execute subscription and return values from stream
/// - stop stream and close ws connection
#[allow(dead_code)]
pub fn graphql_subscriptions<Query, Mutation, Subscription, Context, S>(
websocket: warp::ws::WebSocket,
coordinator: Arc<Coordinator<'static, Query, Mutation, Subscription, Context, S>>,
context: Context,
) -> impl Future<Output = ()> + Send
where
S: ScalarValue + Send + Sync + 'static,
Context: Clone + Send + Sync + 'static,
Query: juniper::GraphQLTypeAsync<S, Context = Context> + Send + Sync + 'static,
Query::TypeInfo: Send + Sync,
Mutation: juniper::GraphQLTypeAsync<S, Context = Context> + Send + Sync + 'static,
Mutation::TypeInfo: Send + Sync,
Subscription:
juniper::GraphQLSubscriptionType<S, Context = Context> + Send + Sync + 'static,
Subscription::TypeInfo: Send + Sync,
{
let (sink_tx, sink_rx) = websocket.split();
let (ws_tx, ws_rx) = mpsc::unbounded();
tokio::task::spawn(
ws_rx
.take_while(|v: &Option<_>| futures::future::ready(v.is_some()))
.map(|x| x.unwrap())
.forward(sink_tx),
);
let context = Arc::new(context);
let got_close_signal = Arc::new(AtomicBool::new(false));
sink_rx.for_each(move |msg| {
let msg = msg.unwrap_or_else(|e| panic!("Websocket receive error: {}", e));
if msg.is_close() {
return futures::future::ready(());
}
let coordinator = coordinator.clone();
let context = context.clone();
let got_close_signal = got_close_signal.clone();
let msg = msg.to_str().expect("Non-text messages are not accepted");
let request: WsPayload<S> = serde_json::from_str(msg).expect("Invalid WsPayload");
match request.type_name.as_str() {
"connection_init" => {}
"start" => {
{
let closed = got_close_signal.load(Ordering::Relaxed);
if closed {
return futures::future::ready(());
}
}
let ws_tx = ws_tx.clone();
tokio::task::spawn(async move {
let payload = request.payload.expect("Could not deserialize payload");
let request_id = request.id.unwrap_or("1".to_owned());
let graphql_request = GraphQLRequest::<S>::new(
payload.query.expect("Could not deserialize query"),
None,
payload.variables,
);
let values_stream =
match coordinator.subscribe(&graphql_request, &context).await {
Ok(s) => s,
Err(err) => {
let _ = ws_tx.unbounded_send(Some(Ok(Message::text(format!(
r#"{{"type":"error","id":"{}","payload":{}}}"#,
request_id,
serde_json::ser::to_string(&err).unwrap_or(
"Error deserializing GraphQLError".to_owned()
)
)))));
let close_message = format!(
r#"{{"type":"complete","id":"{}","payload":null}}"#,
request_id
);
let _ = ws_tx
.unbounded_send(Some(Ok(Message::text(close_message))));
// close channel
let _ = ws_tx.unbounded_send(None);
return;
}
};
values_stream
.take_while(move |response| {
let request_id = request_id.clone();
let closed = got_close_signal.load(Ordering::Relaxed);
if !closed {
let mut response_text = serde_json::to_string(&response)
.unwrap_or("Error deserializing respone".to_owned());
response_text = format!(
r#"{{"type":"data","id":"{}","payload":{} }}"#,
request_id, response_text
);
let _ = ws_tx
.unbounded_send(Some(Ok(Message::text(response_text))));
}
async move { !closed }
})
.for_each(|_| async {})
.await;
});
}
"stop" => {
got_close_signal.store(true, Ordering::Relaxed);
let request_id = request.id.unwrap_or("1".to_owned());
let close_message = format!(
r#"{{"type":"complete","id":"{}","payload":null}}"#,
request_id
);
let _ = ws_tx.unbounded_send(Some(Ok(Message::text(close_message))));
// close channel
let _ = ws_tx.unbounded_send(None);
}
_ => {}
}
futures::future::ready(())
})
}
#[derive(Deserialize)]
#[serde(bound = "GraphQLPayload<S>: Deserialize<'de>")]
struct WsPayload<S>
where
S: ScalarValue + Send + Sync + 'static,
{
id: Option<String>,
#[serde(rename(deserialize = "type"))]
type_name: String,
payload: Option<GraphQLPayload<S>>,
}
#[derive(Debug, Deserialize)]
#[serde(bound = "InputValue<S>: Deserialize<'de>")]
struct GraphQLPayload<S>
where
S: ScalarValue + Send + Sync + 'static,
{
variables: Option<InputValue<S>>,
extensions: Option<HashMap<String, String>>,
#[serde(rename(deserialize = "operationName"))]
operaton_name: Option<String>,
query: Option<String>,
}
#[derive(Serialize)]
struct Output {
data: String,
variables: String,
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -416,23 +635,24 @@ mod tests {
graphiql_response("/abcd");
}
#[test]
fn graphiql_endpoint_matches() {
let filter = warp::get2()
#[tokio::test]
async fn graphiql_endpoint_matches() {
let filter = warp::get()
.and(warp::path("graphiql"))
.and(graphiql_filter("/graphql"));
let result = request()
.method("GET")
.path("/graphiql")
.header("accept", "text/html")
.filter(&filter);
.filter(&filter)
.await;
assert!(result.is_ok());
}
#[test]
fn graphiql_endpoint_returns_graphiql_source() {
let filter = warp::get2()
#[tokio::test]
async fn graphiql_endpoint_returns_graphiql_source() {
let filter = warp::get()
.and(warp::path("dogs-api"))
.and(warp::path("graphiql"))
.and(graphiql_filter("/dogs-api/graphql"));
@ -440,7 +660,8 @@ mod tests {
.method("GET")
.path("/dogs-api/graphiql")
.header("accept", "text/html")
.reply(&filter);
.reply(&filter)
.await;
assert_eq!(response.status(), http::StatusCode::OK);
assert_eq!(
@ -452,31 +673,37 @@ mod tests {
assert!(body.contains("<script>var GRAPHQL_URL = '/dogs-api/graphql';</script>"));
}
#[test]
fn playground_endpoint_matches() {
let filter = warp::get2()
#[tokio::test]
async fn playground_endpoint_matches() {
let filter = warp::get()
.and(warp::path("playground"))
.and(playground_filter("/graphql"));
.and(playground_filter("/graphql", Some("/subscripitons")));
let result = request()
.method("GET")
.path("/playground")
.header("accept", "text/html")
.filter(&filter);
.filter(&filter)
.await;
assert!(result.is_ok());
}
#[test]
fn playground_endpoint_returns_playground_source() {
let filter = warp::get2()
#[tokio::test]
async fn playground_endpoint_returns_playground_source() {
let filter = warp::get()
.and(warp::path("dogs-api"))
.and(warp::path("playground"))
.and(playground_filter("/dogs-api/graphql"));
.and(playground_filter(
"/dogs-api/graphql",
Some("/dogs-api/subscriptions"),
));
let response = request()
.method("GET")
.path("/dogs-api/playground")
.header("accept", "text/html")
.reply(&filter);
.reply(&filter)
.await;
assert_eq!(response.status(), http::StatusCode::OK);
assert_eq!(
@ -485,19 +712,24 @@ mod tests {
);
let body = String::from_utf8(response.body().to_vec()).unwrap();
assert!(body.contains("GraphQLPlayground.init(root, { endpoint: '/dogs-api/graphql' })"));
assert!(body.contains("GraphQLPlayground.init(root, { endpoint: '/dogs-api/graphql', subscriptionEndpoint: '/dogs-api/subscriptions' })"));
}
#[test]
fn graphql_handler_works_json_post() {
#[tokio::test]
async fn graphql_handler_works_json_post() {
use juniper::{
tests::{model::Database, schema::Query},
EmptyMutation, RootNode,
EmptyMutation, EmptySubscription, RootNode,
};
type Schema = juniper::RootNode<'static, Query, EmptyMutation<Database>>;
type Schema =
juniper::RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>;
let schema: Schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema: Schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
let state = warp::any().map(move || Database::new());
let filter = warp::path("graphql2").and(make_graphql_filter(schema, state.boxed()));
@ -508,7 +740,8 @@ mod tests {
.header("accept", "application/json")
.header("content-type", "application/json")
.body(r##"{ "variables": null, "query": "{ hero(episode: NEW_HOPE) { name } }" }"##)
.reply(&filter);
.reply(&filter)
.await;
assert_eq!(response.status(), http::StatusCode::OK);
assert_eq!(
@ -521,16 +754,21 @@ mod tests {
);
}
#[test]
fn batch_requests_work() {
#[tokio::test]
async fn batch_requests_work() {
use juniper::{
tests::{model::Database, schema::Query},
EmptyMutation, RootNode,
EmptyMutation, EmptySubscription, RootNode,
};
type Schema = juniper::RootNode<'static, Query, EmptyMutation<Database>>;
type Schema =
juniper::RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>;
let schema: Schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let schema: Schema = RootNode::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
);
let state = warp::any().map(move || Database::new());
let filter = warp::path("graphql2").and(make_graphql_filter(schema, state.boxed()));
@ -546,7 +784,8 @@ mod tests {
{ "variables": null, "query": "{ hero(episode: EMPIRE) { id name } }" }
]"##,
)
.reply(&filter);
.reply(&filter)
.await;
assert_eq!(response.status(), http::StatusCode::OK);
assert_eq!(
@ -568,92 +807,100 @@ mod tests {
}
}
#[cfg(test)]
mod tests_http_harness {
use super::*;
use juniper::{
http::tests::{run_http_test_suite, HTTPIntegration, TestResponse},
tests::{model::Database, schema::Query},
EmptyMutation, RootNode,
};
use warp::{self, Filter};
type Schema = juniper::RootNode<'static, Query, EmptyMutation<Database>>;
fn warp_server() -> warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)> {
let schema: Schema = RootNode::new(Query, EmptyMutation::<Database>::new());
let state = warp::any().map(move || Database::new());
let filter = warp::filters::path::end().and(make_graphql_filter(schema, state.boxed()));
filter.boxed()
}
struct TestWarpIntegration {
filter: warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)>,
}
// This can't be implemented with the From trait since TestResponse is not defined in this crate.
fn test_response_from_http_response(response: warp::http::Response<Vec<u8>>) -> TestResponse {
TestResponse {
status_code: response.status().as_u16() as i32,
body: Some(String::from_utf8(response.body().to_owned()).unwrap()),
content_type: response
.headers()
.get("content-type")
.expect("missing content-type header in warp response")
.to_str()
.expect("invalid content-type string")
.to_owned(),
}
}
impl HTTPIntegration for TestWarpIntegration {
fn get(&self, url: &str) -> TestResponse {
use percent_encoding::{percent_encode, DEFAULT_ENCODE_SET};
let url: String = percent_encode(url.replace("/?", "").as_bytes(), DEFAULT_ENCODE_SET)
.into_iter()
.collect::<Vec<_>>()
.join("");
let response = warp::test::request()
.method("GET")
.path(&format!("/?{}", url))
.filter(&self.filter)
.unwrap_or_else(|rejection| {
warp::http::Response::builder()
.status(rejection.status())
.header("content-type", "application/json")
.body(Vec::new())
.unwrap()
});
test_response_from_http_response(response)
}
fn post(&self, url: &str, body: &str) -> TestResponse {
let response = warp::test::request()
.method("POST")
.header("content-type", "application/json")
.path(url)
.body(body)
.filter(&self.filter)
.unwrap_or_else(|rejection| {
warp::http::Response::builder()
.status(rejection.status())
.header("content-type", "application/json")
.body(Vec::new())
.unwrap()
});
test_response_from_http_response(response)
}
}
#[test]
fn test_warp_integration() {
let integration = TestWarpIntegration {
filter: warp_server(),
};
run_http_test_suite(&integration);
}
}
//TODO: update warp tests
//#[cfg(test)]
//mod tests_http_harness {
// use super::*;
// use juniper::{
// http::tests::{run_http_test_suite, HTTPIntegration, TestResponse},
// tests::{model::Database, schema::Query},
// EmptyMutation, EmptySubscription, RootNode,
// };
// use warp::{self, Filter};
//
// type Schema =
// juniper::RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>;
//
// fn warp_server() -> warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)> {
// let schema: Schema = RootNode::new(
// Query,
// EmptyMutation::<Database>::new(),
// EmptySubscription::<Database>::new(),
// );
//
// let state = warp::any().map(move || Database::new());
// let filter = warp::filters::path::end().and(make_graphql_filter(schema, state.boxed()));
//
// filter.boxed()
// }
//
// struct TestWarpIntegration {
// filter: warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)>,
// }
//
// // This can't be implemented with the From trait since TestResponse is not defined in this crate.
// fn test_response_from_http_response(response: warp::http::Response<Vec<u8>>) -> TestResponse {
// TestResponse {
// status_code: response.status().as_u16() as i32,
// body: Some(String::from_utf8(response.body().to_owned()).unwrap()),
// content_type: response
// .headers()
// .get("content-type")
// .expect("missing content-type header in warp response")
// .to_str()
// .expect("invalid content-type string")
// .to_owned(),
// }
// }
//
// impl HTTPIntegration for TestWarpIntegration {
// fn get(&self, url: &str) -> TestResponse {
// use percent_encoding::{percent_encode, DEFAULT_ENCODE_SET};
// let url: String = percent_encode(url.replace("/?", "").as_bytes(), DEFAULT_ENCODE_SET)
// .into_iter()
// .collect::<Vec<_>>()
// .join("");
//
// let response = warp::test::request()
// .method("GET")
// .path(&format!("/?{}", url))
// .filter(&self.filter)
// .await
// .unwrap_or_else(|rejection| {
// warp::http::Response::builder()
// .status(rejection.status())
// .header("content-type", "application/json")
// .body(Vec::new())
// .unwrap()
// });
// test_response_from_http_response(response)
// }
//
// fn post(&self, url: &str, body: &str) -> TestResponse {
// let response = warp::test::request()
// .method("POST")
// .header("content-type", "application/json")
// .path(url)
// .body(body)
// .filter(&self.filter)
// .await
// .unwrap_or_else(|rejection| {
// warp::http::Response::builder()
// .status(rejection.status())
// .header("content-type", "application/json")
// .body(Vec::new())
// .unwrap()
// });
// test_response_from_http_response(response)
// }
// }
//
// #[test]
// fn test_warp_integration() {
// let integration = TestWarpIntegration {
// filter: warp_server(),
// };
//
// run_http_test_suite(&integration);
// }
//}