diff --git a/Cargo.toml b/Cargo.toml index 5c4737ed9..6f10451ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", ] diff --git a/benches/bench.rs b/benches/bench.rs index 994641fde..c194b0d01 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -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::::new()); + let schema = RootNode::new( + &database, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + &database, + EmptyMutation::::new(), + EmptySubscription::::new(), + ); let doc = r#" query IntrospectionQuery { diff --git a/docs/book/content/advanced/introspection.md b/docs/book/content/advanced/introspection.md index 34d1bf037..8effba29d 100644 --- a/docs/book/content/advanced/introspection.md +++ b/docs/book/content/advanced/introspection.md @@ -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>; +type Schema = juniper::RootNode< + 'static, + Query, + EmptyMutation, + EmptySubscription +>; 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(); diff --git a/docs/book/content/quickstart.md b/docs/book/content/quickstart.md index 25846a3a4..ab2f02c00 100644 --- a/docs/book/content/quickstart.md +++ b/docs/book/content/quickstart.md @@ -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>; # 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>; +type Schema = juniper::RootNode<'static, Query, EmptyMutation, EmptySubscription>; 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(); diff --git a/examples/warp_async/Cargo.toml b/examples/warp_async/Cargo.toml index 169edb501..ae9b0d71a 100644 --- a/examples/warp_async/Cargo.toml +++ b/examples/warp_async/Cargo.toml @@ -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"] } diff --git a/examples/warp_async/src/main.rs b/examples/warp_async/src/main.rs index e81a36dd2..7bc142c5e 100644 --- a/examples/warp_async/src/main.rs +++ b/examples/warp_async/src/main.rs @@ -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>; +type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>; fn schema() -> Schema { - Schema::new(Query, EmptyMutation::::new()) + Schema::new(Query, EmptyMutation::::new(), EmptySubscription::::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 } diff --git a/examples/warp_subscriptions/.gitignore b/examples/warp_subscriptions/.gitignore new file mode 100644 index 000000000..eb5a316cb --- /dev/null +++ b/examples/warp_subscriptions/.gitignore @@ -0,0 +1 @@ +target diff --git a/examples/warp_subscriptions/Cargo.toml b/examples/warp_subscriptions/Cargo.toml new file mode 100644 index 000000000..159df10a8 --- /dev/null +++ b/examples/warp_subscriptions/Cargo.toml @@ -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"] } diff --git a/examples/warp_subscriptions/src/main.rs b/examples/warp_subscriptions/src/main.rs new file mode 100644 index 000000000..864536089 --- /dev/null +++ b/examples/warp_subscriptions/src/main.rs @@ -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 { + 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 { + vec![User { + id, + kind: UserKind::Admin, + name: "User Name".into(), + }] + } +} + +type UsersStream = Pin> + 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, 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!( + "

juniper_subscriptions demo

visit graphql playground" + )) + }); + + 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>| { + ws.on_upgrade(|websocket| -> Pin + 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; +} diff --git a/integration_tests/async_await/src/main.rs b/integration_tests/async_await/src/main.rs index 46229e3f1..61d47c0cb 100644 --- a/integration_tests/async_await/src/main.rs +++ b/integration_tests/async_await/src/main.rs @@ -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 diff --git a/integration_tests/juniper_tests/src/codegen/derive_object.rs b/integration_tests/juniper_tests/src/codegen/derive_object.rs index cbeaac7f1..6e62489d6 100644 --- a/integration_tests/juniper_tests/src/codegen/derive_object.rs +++ b/integration_tests/juniper_tests/src/codegen/derive_object.rs @@ -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(doc: &str, f: F) where F: Fn((&Object, &Vec)) -> (), { - 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 diff --git a/integration_tests/juniper_tests/src/codegen/derive_object_with_raw_idents.rs b/integration_tests/juniper_tests/src/codegen/derive_object_with_raw_idents.rs index 763f89afa..29a10174d 100644 --- a/integration_tests/juniper_tests/src/codegen/derive_object_with_raw_idents.rs +++ b/integration_tests/juniper_tests/src/codegen/derive_object_with_raw_idents.rs @@ -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 diff --git a/integration_tests/juniper_tests/src/custom_scalar.rs b/integration_tests/juniper_tests/src/custom_scalar.rs index a1333e6cc..33418adcf 100644 --- a/integration_tests/juniper_tests/src/custom_scalar.rs +++ b/integration_tests/juniper_tests/src/custom_scalar.rs @@ -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(query: &str, vars: Variables, f: F where F: Fn(&Object) -> (), { - 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 diff --git a/integration_tests/juniper_tests/src/issue_371.rs b/integration_tests/juniper_tests/src/issue_371.rs index e2bcdf06c..298ee1e97 100644 --- a/integration_tests/juniper_tests/src/issue_371.rs +++ b/integration_tests/juniper_tests/src/issue_371.rs @@ -48,7 +48,7 @@ impl Country { } } -type Schema = juniper::RootNode<'static, Query, EmptyMutation>; +type Schema = juniper::RootNode<'static, Query, EmptyMutation, EmptySubscription>; #[tokio::test] async fn users() { @@ -59,7 +59,11 @@ async fn users() { let (_, errors) = juniper::execute( query, None, - &Schema::new(Query, EmptyMutation::::new()), + &Schema::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()), + &Schema::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()), + &Schema::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + ), &juniper::Variables::new(), &ctx, ) diff --git a/integration_tests/juniper_tests/src/issue_398.rs b/integration_tests/juniper_tests/src/issue_398.rs index 27b6a6cd9..bb6299493 100644 --- a/integration_tests/juniper_tests/src/issue_398.rs +++ b/integration_tests/juniper_tests/src/issue_398.rs @@ -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(), &(), ) diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index ae55e77c2..4278e8584 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -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) diff --git a/juniper/release.toml b/juniper/release.toml index 53f497bd2..2ae9555f4 100644 --- a/juniper/release.toml +++ b/juniper/release.toml @@ -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}}\""}, ] diff --git a/juniper/src/executor/look_ahead.rs b/juniper/src/executor/look_ahead.rs index ad045da06..c9dbba6b8 100644 --- a/juniper/src/executor/look_ahead.rs +++ b/juniper/src/executor/look_ahead.rs @@ -185,7 +185,7 @@ where pub(super) fn build_from_selection( s: &'a Selection<'a, S>, vars: &'a Variables, - fragments: &'a HashMap<&'a str, &'a Fragment<'a, S>>, + fragments: &'a HashMap<&'a str, Fragment<'a, S>>, ) -> Option> { 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, - fragments: &'a HashMap<&'a str, &'a Fragment<'a, S>>, + fragments: &'a HashMap<&'a str, Fragment<'a, S>>, ) -> Option> { let empty: &[Selection] = &[]; 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::(&(), &())) + crate::parse_document_source( + q, + &SchemaType::new::(&(), &(), &()), + ) } - fn extract_fragments<'a, S>(doc: &'a Document) -> HashMap<&'a str, &'a Fragment<'a, S>> { + fn extract_fragments<'a, S>(doc: &'a Document) -> 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); } } diff --git a/juniper/src/executor/mod.rs b/juniper/src/executor/mod.rs index 5b3d29fb5..ad94b3600 100644 --- a/juniper/src/executor/mod.rs +++ b/juniper/src/executor/mod.rs @@ -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>), } /// 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, - 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, + 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>>, - field_path: FieldPath<'a>, + errors: &'r RwLock>>, + field_path: Arc>, } /// Error type for errors that occur during query execution @@ -220,6 +219,10 @@ pub type FieldResult = Result>; /// The result of resolving an unspecified field pub type ExecutionResult = Result, FieldError>; +/// Boxed `futures::Stream` yielding `Result, ExecutionError>` +pub type ValuesStream<'a, S = DefaultScalarValue> = + std::pin::Pin, ExecutionError>> + Send + 'a>>; + /// The map of variables used for substitution during query execution pub type Variables = HashMap>; @@ -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> + where + 'i: 'res, + 'v: 'res, + 'a: 'res, + T: crate::GraphQLSubscriptionType + 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>, FieldError> + where + 't: 'res, + 'a: 'res, + T: crate::GraphQLSubscriptionType, + 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(&self, info: &T::TypeInfo, value: &T) -> ExecutionResult 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]>, - ) -> Executor { + 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]>, - ) -> Executor { + 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 { + pub fn variables(&self) -> &'r Variables { 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) -> ExecutionError { + 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) { - 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 ExecutionError { } } -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, operation: &'b Spanning>, - root_node: &RootNode, + root_node: &RootNode, variables: &Variables, context: &CtxT, ) -> Result<(Value, Vec>), GraphQLError<'a>> @@ -667,7 +761,12 @@ where S: ScalarValue, QueryT: GraphQLType, MutationT: GraphQLType, + SubscriptionT: GraphQLType, { + 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>, - root_node: &RootNode<'a, QueryT, MutationT, S>, + root_node: &RootNode<'a, QueryT, MutationT, SubscriptionT, S>, variables: &Variables, context: &CtxT, ) -> Result<(Value, Vec>), GraphQLError<'a>> @@ -756,8 +857,14 @@ where QueryT::TypeInfo: Send + Sync, MutationT: crate::GraphQLTypeAsync + Send + Sync, MutationT::TypeInfo: Send + Sync, + SubscriptionT: GraphQLType + 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>, GraphQLError<'a>> +) -> Result<&'b Spanning>, 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>, + root_node: &'r RootNode<'r, QueryT, MutationT, SubscriptionT, S>, + variables: &Variables, + context: &'r CtxT, +) -> Result<(Value>, Vec>), GraphQLError<'r>> +where + 'r: 'exec_ref, + 'd: 'r, + 'op: 'd, + S: ScalarValue + Send + Sync, + QueryT: crate::GraphQLTypeAsync + Send + Sync, + QueryT::TypeInfo: Send + Sync, + MutationT: crate::GraphQLTypeAsync + Send + Sync, + MutationT::TypeInfo: Send + Sync, + SubscriptionT: crate::GraphQLSubscriptionType + 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::>>() + }); + + 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, diff --git a/juniper/src/executor/owned_executor.rs b/juniper/src/executor/owned_executor.rs new file mode 100644 index 000000000..366425f20 --- /dev/null +++ b/juniper/src/executor/owned_executor.rs @@ -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, + pub(super) current_selection_set: Option>>, + pub(super) parent_selection_set: Option>>, + pub(super) current_type: TypeType<'a, S>, + pub(super) schema: &'a SchemaType<'a, S>, + pub(super) context: &'a CtxT, + pub(super) errors: RwLock>>, + pub(super) field_path: Arc>, +} + +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>>, + ) -> 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 { + self.variables.clone() + } + + #[doc(hidden)] + pub fn field_sub_executor( + &self, + field_alias: &'a str, + field_name: &'a str, + location: SourcePosition, + selection_set: Option>>, + ) -> 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 { + self.schema + } + + #[doc(hidden)] + pub fn location(&self) -> &SourcePosition { + self.field_path.location() + } +} diff --git a/juniper/src/executor_tests/async_await/mod.rs b/juniper/src/executor_tests/async_await/mod.rs index 28d5fb739..aa352321b 100644 --- a/juniper/src/executor_tests/async_await/mod.rs +++ b/juniper/src/executor_tests/async_await/mod.rs @@ -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 diff --git a/juniper/src/executor_tests/directives.rs b/juniper/src/executor_tests/directives.rs index d0de8a8c8..eb4fa1f24 100644 --- a/juniper/src/executor_tests/directives.rs +++ b/juniper/src/executor_tests/directives.rs @@ -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(query: &str, vars: Variables, where F: Fn(&Object) -> (), { - 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 diff --git a/juniper/src/executor_tests/enums.rs b/juniper/src/executor_tests/enums.rs index 88385b1da..996013388 100644 --- a/juniper/src/executor_tests/enums.rs +++ b/juniper/src/executor_tests/enums.rs @@ -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(query: &str, vars: Variables, where F: Fn(&Object) -> (), { - 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))] diff --git a/juniper/src/executor_tests/executor.rs b/juniper/src/executor_tests/executor.rs index 87bef8372..4d335eb6c 100644 --- a/juniper/src/executor_tests/executor.rs +++ b/juniper/src/executor_tests/executor.rs @@ -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::::new()); + let schema = RootNode::new( + Schema, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Schema, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Schema, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Schema, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Schema, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Schema, + EmptyMutation::::new(), + EmptySubscription::::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(); diff --git a/juniper/src/executor_tests/interfaces_unions.rs b/juniper/src/executor_tests/interfaces_unions.rs index 63ba8255b..51a17ed9f 100644 --- a/juniper/src/executor_tests/interfaces_unions.rs +++ b/juniper/src/executor_tests/interfaces_unions.rs @@ -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" { diff --git a/juniper/src/executor_tests/introspection/enums.rs b/juniper/src/executor_tests/introspection/enums.rs index 8cfbcff88..5cd37d5e6 100644 --- a/juniper/src/executor_tests/introspection/enums.rs +++ b/juniper/src/executor_tests/introspection/enums.rs @@ -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(doc: &str, f: F) where F: Fn((&Object, &Vec>)) -> (), { - 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 diff --git a/juniper/src/executor_tests/introspection/input_object.rs b/juniper/src/executor_tests/introspection/input_object.rs index d7970ae7a..d65a4a955 100644 --- a/juniper/src/executor_tests/introspection/input_object.rs +++ b/juniper/src/executor_tests/introspection/input_object.rs @@ -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(doc: &str, f: F) where F: Fn(&Object, &Vec>) -> (), { - 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 diff --git a/juniper/src/executor_tests/introspection/mod.rs b/juniper/src/executor_tests/introspection/mod.rs index 1c680d66d..8c0098c02 100644 --- a/juniper/src/executor_tests/introspection/mod.rs +++ b/juniper/src/executor_tests/introspection/mod.rs @@ -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 diff --git a/juniper/src/executor_tests/variables.rs b/juniper/src/executor_tests/variables.rs index ad91b2e88..a04b345f2 100644 --- a/juniper/src/executor_tests/variables.rs +++ b/juniper/src/executor_tests/variables.rs @@ -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(query: &str, vars: Variables, where F: Fn(&Object) -> (), { - 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"))] diff --git a/juniper/src/http/mod.rs b/juniper/src/http/mod.rs index 1209780ed..896d88552 100644 --- a/juniper/src/http/mod.rs +++ b/juniper/src/http/mod.rs @@ -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, + root_node: &'a RootNode, context: &CtxT, ) -> GraphQLResponse<'a, S> where S: ScalarValue, QueryT: GraphQLType, MutationT: GraphQLType, + SubscriptionT: GraphQLType, { 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 + Send + Sync, MutationT::TypeInfo: Send + Sync, + SubscriptionT: GraphQLType + 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` 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, + root_node: &'rn RootNode<'a, QueryT, MutationT, SubscriptionT, S>, + context: &'ctx CtxT, +) -> Result<(Value>, Vec>), GraphQLError<'a>> +where + 'req: 'a, + 'rn: 'a, + 'ctx: 'a, + S: ScalarValue + Send + Sync + 'static, + QueryT: GraphQLTypeAsync + Send + Sync, + QueryT::TypeInfo: Send + Sync, + MutationT: GraphQLTypeAsync + Send + Sync, + MutationT::TypeInfo: Send + Sync, + SubscriptionT: GraphQLSubscriptionType + 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, Vec>), GraphQLError<'a>>) -> Self { + Self(r) + } + /// Constructs an error response outside of the normal execution flow pub fn error(error: FieldError) -> Self { GraphQLResponse(Ok((Value::null(), vec![ExecutionError::at_origin(error)]))) diff --git a/juniper/src/http/playground.rs b/juniper/src/http/playground.rs index 08e674183..af083914e 100644 --- a/juniper/src/http/playground.rs +++ b/juniper/src/http/playground.rs @@ -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##" @@ -14,7 +23,7 @@ pub fn playground_source(graphql_endpoint_url: &str) -> String { GraphQL Playground - + @@ -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' }) }) "##.replace("JUNIPER_GRAPHQL_URL", graphql_endpoint_url) + .replace("JUNIPER_SUBSCRIPTIONS_URL", subscriptions_endpoint) } diff --git a/juniper/src/integrations/chrono.rs b/juniper/src/integrations/chrono.rs index fc57e8bd3..5be416be3 100644 --- a/juniper/src/integrations/chrono.rs +++ b/juniper/src/integrations/chrono.rs @@ -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 diff --git a/juniper/src/integrations/serde.rs b/juniper/src/integrations/serde.rs index 97b465096..126c9b717 100644 --- a/juniper/src/integrations/serde.rs +++ b/juniper/src/integrations/serde.rs @@ -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), } } } diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index 05d7331fa..4d51f1f70 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -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 + '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, + root_node: &'a RootNode, variables: &Variables, context: &CtxT, ) -> Result<(Value, Vec>), GraphQLError<'a>> @@ -233,6 +236,7 @@ where S: ScalarValue, QueryT: GraphQLType, MutationT: GraphQLType, + SubscriptionT: GraphQLType, { 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, context: &CtxT, ) -> Result<(Value, Vec>), GraphQLError<'a>> @@ -273,6 +277,8 @@ where QueryT::TypeInfo: Send + Sync, MutationT: GraphQLTypeAsync + Send + Sync, MutationT::TypeInfo: Send + Sync, + SubscriptionT: GraphQLType + 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, + context: &'a CtxT, +) -> Result<(Value>, Vec>), GraphQLError<'a>> +where + S: ScalarValue + Send + Sync, + QueryT: GraphQLTypeAsync + Send + Sync, + QueryT::TypeInfo: Send + Sync, + MutationT: GraphQLTypeAsync + Send + Sync, + MutationT::TypeInfo: Send + Sync, + SubscriptionT: GraphQLSubscriptionType + 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, +pub fn introspect<'a, S, CtxT, QueryT, MutationT, SubscriptionT>( + root_node: &'a RootNode, context: &CtxT, format: IntrospectionFormat, ) -> Result<(Value, Vec>), GraphQLError<'a>> @@ -311,6 +352,7 @@ where S: ScalarValue, QueryT: GraphQLType, MutationT: GraphQLType, + SubscriptionT: GraphQLType, { execute_sync( match format { diff --git a/juniper/src/macros/interface.rs b/juniper/src/macros/interface.rs index b0c6fa4a9..e0c399f54 100644 --- a/juniper/src/macros/interface.rs +++ b/juniper/src/macros/interface.rs @@ -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| { diff --git a/juniper/src/macros/mod.rs b/juniper/src/macros/mod.rs index 7e6ce8eb5..a572ba7e0 100644 --- a/juniper/src/macros/mod.rs +++ b/juniper/src/macros/mod.rs @@ -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; diff --git a/juniper/src/macros/subscription_helpers.rs b/juniper/src/macros/subscription_helpers.rs new file mode 100644 index 000000000..6ca55f459 --- /dev/null +++ b/juniper/src/macros/subscription_helpers.rs @@ -0,0 +1,99 @@ +//! Helper types for converting types to `Result>`. +//! +//! 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 and then a function on Stream should be called. +pub trait IntoFieldResult { + /// Turn current type into a generic result + fn into_result(self) -> Result>; +} + +impl IntoFieldResult for Result +where + E: Into>, +{ + fn into_result(self) -> Result> { + self.map_err(|e| e.into()) + } +} + +impl IntoFieldResult for T +where + T: Stream, +{ + fn into_result(self) -> Result> { + 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 +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; +} + +impl ExtractTypeFromStream for T +where + T: futures::Stream, + I: GraphQLType, + S: ScalarValue, +{ + type Item = I; +} + +impl ExtractTypeFromStream for Ty +where + Ty: futures::Stream>, + T: GraphQLType, + S: ScalarValue, +{ + type Item = T; +} + +impl ExtractTypeFromStream for Result +where + T: futures::Stream, + I: GraphQLType, + S: ScalarValue, +{ + type Item = I; +} + +impl ExtractTypeFromStream for Result +where + T: futures::Stream>, + I: GraphQLType, + S: ScalarValue, +{ + type Item = I; +} diff --git a/juniper/src/macros/tests/args.rs b/juniper/src/macros/tests/args.rs index 209b1bd6a..d7d9ffecb 100644 --- a/juniper/src/macros/tests/args.rs +++ b/juniper/src/macros/tests/args.rs @@ -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 diff --git a/juniper/src/macros/tests/field.rs b/juniper/src/macros/tests/field.rs index 07fe5ec17..aed00f616 100644 --- a/juniper/src/macros/tests/field.rs +++ b/juniper/src/macros/tests/field.rs @@ -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(); diff --git a/juniper/src/macros/tests/impl_object.rs b/juniper/src/macros/tests/impl_object.rs index 0e3ff3e6b..cbbc78061 100644 --- a/juniper/src/macros/tests/impl_object.rs +++ b/juniper/src/macros/tests/impl_object.rs @@ -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").await; + let res = util::run_info_query::("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::::new()); + let schema = RootNode::new( + Query { b: true }, + EmptyMutation::::new(), + EmptySubscription::::new(), + ); let vars = std::collections::HashMap::new(); let (result, errs) = crate::execute(doc, None, &schema, &vars, &Context { flag1: true }) diff --git a/juniper/src/macros/tests/impl_subscription.rs b/juniper/src/macros/tests/impl_subscription.rs new file mode 100644 index 000000000..45c38996b --- /dev/null +++ b/juniper/src/macros/tests/impl_subscription.rs @@ -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 = Pin + 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 { + let b = self.b; + Box::pin(futures::stream::once(async move { b })) + } + + async fn independent() -> Stream { + Box::pin(futures::stream::once(async { 100 })) + } + + async fn with_executor(_exec: &Executor) -> Stream { + Box::pin(futures::stream::once(async { true })) + } + + async fn with_executor_and_self(&self, _exec: &Executor) -> Stream { + Box::pin(futures::stream::once(async { true })) + } + + async fn with_context(_context: &Context) -> Stream { + Box::pin(futures::stream::once(async { true })) + } + + async fn with_context_and_self(&self, _context: &Context) -> Stream { + Box::pin(futures::stream::once(async { true })) + } + + #[graphql(name = "renamed")] + async fn has_custom_name() -> Stream { + Box::pin(futures::stream::once(async { true })) + } + + #[graphql(description = "attr")] + async fn has_description_attr() -> Stream { + Box::pin(futures::stream::once(async { true })) + } + + /// Doc description + async fn has_description_doc_comment() -> Stream { + Box::pin(futures::stream::once(async { true })) + } + + async fn has_argument(arg1: bool) -> Stream { + Box::pin(futures::stream::once(async move { arg1 })) + } + + #[graphql(arguments(default_arg(default = true)))] + async fn default_argument(default_arg: bool) -> Stream { + Box::pin(futures::stream::once(async move { default_arg })) + } + + #[graphql(arguments(arg(description = "my argument description")))] + async fn arg_with_description(arg: bool) -> Stream { + Box::pin(futures::stream::once(async move { arg })) + } + + async fn with_context_child(&self) -> Stream { + Box::pin(futures::stream::once(async { WithContext })) + } + + async fn with_implicit_lifetime_child(&self) -> Stream> { + Box::pin(futures::stream::once(async { + WithLifetime { value: "blub" } + })) + } + + async fn with_mut_arg(mut arg: bool) -> Stream { + if arg { + arg = !arg; + } + + Box::pin(futures::stream::once(async move { arg })) + } + + async fn without_type_alias() -> Pin + Send>> { + Box::pin(futures::stream::once(async { "abc" })) + } +} + +#[tokio::test] +async fn object_introspect() { + let res = util::run_info_query::("Subscription").await; + assert_eq!( + res, + crate::graphql_value!({ + "name": "Subscription", + "description": "Subscription Description.", + "fields": [ + { + "name": "withSelf", + "description": "With Self Description", + "args": [], + }, + { + "name": "independent", + "description": None, + "args": [], + }, + { + "name": "withExecutor", + "description": None, + "args": [], + }, + { + "name": "withExecutorAndSelf", + "description": None, + "args": [], + }, + { + "name": "withContext", + "description": None, + "args": [], + }, + { + "name": "withContextAndSelf", + "description": None, + "args": [], + }, + { + "name": "renamed", + "description": None, + "args": [], + }, + { + "name": "hasDescriptionAttr", + "description": "attr", + "args": [], + }, + { + "name": "hasDescriptionDocComment", + "description": "Doc description", + "args": [], + }, + { + "name": "hasArgument", + "description": None, + "args": [ + { + "name": "arg1", + "description": None, + "type": { + "name": None, + }, + } + ], + }, + { + "name": "defaultArgument", + "description": None, + "args": [ + { + "name": "defaultArg", + "description": None, + "type": { + "name": "Boolean", + }, + } + ], + }, + { + "name": "argWithDescription", + "description": None, + "args": [ + { + "name": "arg", + "description": "my argument description", + "type": { + "name": None + }, + } + ], + }, + { + "name": "withContextChild", + "description": None, + "args": [], + }, + { + "name": "withImplicitLifetimeChild", + "description": None, + "args": [], + }, + { + "name": "withMutArg", + "description": None, + "args": [ + { + "name": "arg", + "description": None, + "type": { + "name": None, + }, + } + ], + }, + { + "name": "withoutTypeAlias", + "description": None, + "args": [], + } + ] + }) + ); +} + +#[tokio::test] +async fn object_query() { + let doc = r#" + subscription { + withSelf + independent + withExecutor + withExecutorAndSelf + withContext + withContextAndSelf + renamed + hasArgument(arg1: true) + defaultArgument + argWithDescription(arg: true) + withContextChild { + ctx + } + withImplicitLifetimeChild { + value + } + withMutArg(arg: true) + withoutTypeAlias + } + "#; + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + Subscription { b: true }, + ); + let vars = std::collections::HashMap::new(); + + let (stream_val, errs) = + crate::resolve_into_stream(doc, None, &schema, &vars, &Context { flag1: true }) + .await + .expect("Execution failed"); + + let result = if let Value::Object(obj) = stream_val { + let mut result = Vec::new(); + for (name, mut val) in obj { + if let Value::Scalar(ref mut stream) = val { + let first = stream + .next() + .await + .expect("Stream does not have the first element") + .expect(&format!("Error resolving {} field", name)); + result.push((name, first)) + } + } + result + } else { + panic!("Expected to get Value::Object ") + }; + + assert_eq!(errs, []); + assert_eq!( + result, + vec![ + ("withSelf".to_string(), graphql_value!(true)), + ("independent".to_string(), graphql_value!(100)), + ("withExecutor".to_string(), graphql_value!(true)), + ("withExecutorAndSelf".to_string(), graphql_value!(true)), + ("withContext".to_string(), graphql_value!(true)), + ("withContextAndSelf".to_string(), graphql_value!(true)), + ("renamed".to_string(), graphql_value!(true)), + ("hasArgument".to_string(), graphql_value!(true)), + ("defaultArgument".to_string(), graphql_value!(true)), + ("argWithDescription".to_string(), graphql_value!(true)), + ( + "withContextChild".to_string(), + graphql_value!({"ctx": true}) + ), + ( + "withImplicitLifetimeChild".to_string(), + graphql_value!({ "value": "blub" }) + ), + ("withMutArg".to_string(), graphql_value!(false)), + ("withoutTypeAlias".to_string(), graphql_value!("abc")), + ] + ); +} diff --git a/juniper/src/macros/tests/interface.rs b/juniper/src/macros/tests/interface.rs index 2400de6f2..02f739628 100644 --- a/juniper/src/macros/tests/interface.rs +++ b/juniper/src/macros/tests/interface.rs @@ -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(); diff --git a/juniper/src/macros/tests/mod.rs b/juniper/src/macros/tests/mod.rs index c630db6f1..6fb9b9073 100644 --- a/juniper/src/macros/tests/mod.rs +++ b/juniper/src/macros/tests/mod.rs @@ -1,6 +1,7 @@ mod args; mod field; mod impl_object; +mod impl_subscription; mod interface; mod object; mod scalar; diff --git a/juniper/src/macros/tests/object.rs b/juniper/src/macros/tests/object.rs index f09e863cd..68f8e7993 100644 --- a/juniper/src/macros/tests/object.rs +++ b/juniper/src/macros/tests/object.rs @@ -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::::new()); + let schema = RootNode::new( + Root {}, + EmptyMutation::::new(), + EmptySubscription::<()>::new(), + ); let vars = vec![("typeName".to_owned(), InputValue::scalar(type_name))] .into_iter() .collect(); diff --git a/juniper/src/macros/tests/scalar.rs b/juniper/src/macros/tests/scalar.rs index 79f624e6d..be578c22c 100644 --- a/juniper/src/macros/tests/scalar.rs +++ b/juniper/src/macros/tests/scalar.rs @@ -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(doc: &str, f: F) where F: Fn(&Object) -> (), { - 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 diff --git a/juniper/src/macros/tests/union.rs b/juniper/src/macros/tests/union.rs index 3f68443cd..6dfbfe754 100644 --- a/juniper/src/macros/tests/union.rs +++ b/juniper/src/macros/tests/union.rs @@ -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(); diff --git a/juniper/src/macros/tests/util.rs b/juniper/src/macros/tests/util.rs index 70bc5193b..058317285 100644 --- a/juniper/src/macros/tests/util.rs +++ b/juniper/src/macros/tests/util.rs @@ -1,13 +1,21 @@ use crate::{DefaultScalarValue, GraphQLTypeAsync, RootNode, Value, Variables}; use std::default::Default; -pub async fn run_query(query: &str) -> Value +pub async fn run_query(query: &str) -> Value where Query: GraphQLTypeAsync + Default, Mutation: GraphQLTypeAsync + Default, - Context: Default + Sync + Send, + Subscription: crate::GraphQLType + + 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(type_name: &str) -> Value +pub async fn run_info_query(type_name: &str) -> Value where Query: GraphQLTypeAsync + Default, Mutation: GraphQLTypeAsync + Default, - Context: Default + Sync + Send, + Subscription: crate::GraphQLType + + Default + + Sync + + Send, + Context: Default + Send + Sync, { let query = format!( r#" @@ -45,7 +57,7 @@ where "#, type_name ); - let result = run_query::(&query).await; + let result = run_query::(&query).await; result .as_object_value() .expect("Result is not an object") diff --git a/juniper/src/parser/tests/document.rs b/juniper/src/parser/tests/document.rs index 6ad46b75f..834f8f45c 100644 --- a/juniper/src/parser/tests/document.rs +++ b/juniper/src/parser/tests/document.rs @@ -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: &str) -> Document where S: ScalarValue, { - parse_document_source(s, &SchemaType::new::(&(), &())) - .expect(&format!("Parse error on input {:#?}", s)) + parse_document_source( + s, + &SchemaType::new::(&(), &(), &()), + ) + .expect(&format!("Parse error on input {:#?}", s)) } fn parse_document_error<'a, S>(s: &'a str) -> Spanning> where S: ScalarValue, { - match parse_document_source::(s, &SchemaType::new::(&(), &())) { + match parse_document_source::( + s, + &SchemaType::new::(&(), &(), &()), + ) { 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::>(&(), &()); + let schema = SchemaType::new::, EmptySubscription<()>>( + &(), + &(), + &(), + ); let parse_result = parse_document_source(r##"{ echo(value: 123.0) }"##, &schema); assert_eq!( diff --git a/juniper/src/parser/tests/value.rs b/juniper/src/parser/tests/value.rs index c8aba174f..f66685364 100644 --- a/juniper/src/parser/tests/value.rs +++ b/juniper/src/parser/tests/value.rs @@ -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::>(&(), &()); + let schema = SchemaType::new::, EmptySubscription<()>>(&(), &(), &()); parse_value_literal(&mut parser, false, &schema, Some(meta)) .expect(&format!("Parse error on input {:#?}", s)) diff --git a/juniper/src/schema/model.rs b/juniper/src/schema/model.rs index 35ba45224..6e87bee9b 100644 --- a/juniper/src/schema/model.rs +++ b/juniper/src/schema/model.rs @@ -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, MutationT: GraphQLType, S = DefaultScalarValue> -where +pub struct RootNode< + 'a, + QueryT: GraphQLType, + MutationT: GraphQLType, + SubscriptionT: GraphQLType, + 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>, query_type_name: String, mutation_type_name: Option, + subscription_type_name: Option, directives: FnvHashMap>, } @@ -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, MutationT: GraphQLType, + SubscriptionT: GraphQLType, { - /// 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, MutationT: GraphQLType, + SubscriptionT: GraphQLType, 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::(&query_info, &mutation_info), + subscription_type: subscription_obj, + schema: SchemaType::new::( + &query_info, + &mutation_info, + &subscription_info, + ), query_info, mutation_info, + subscription_info, } } } impl<'a, S> SchemaType<'a, S> { - pub fn new( + pub fn new( query_info: &QueryT::TypeInfo, mutation_info: &MutationT::TypeInfo, + subscription_info: &SubscriptionT::TypeInfo, ) -> Self where S: ScalarValue + 'a, QueryT: GraphQLType, MutationT: GraphQLType, + SubscriptionT: GraphQLType, { 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::(subscription_info) + .innermost_name() + .to_owned(); + registry.get_type::>(&()); 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> { - // 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> { - // 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> { diff --git a/juniper/src/schema/schema.rs b/juniper/src/schema/schema.rs index e10d80377..39a155732 100644 --- a/juniper/src/schema/schema.rs +++ b/juniper/src/schema/schema.rs @@ -13,11 +13,13 @@ use crate::schema::{ model::{DirectiveLocation, DirectiveType, RootNode, SchemaType, TypeType}, }; -impl<'a, CtxT, S, QueryT, MutationT> GraphQLType for RootNode<'a, QueryT, MutationT, S> +impl<'a, CtxT, S, QueryT, MutationT, SubscriptionT> GraphQLType + for RootNode<'a, QueryT, MutationT, SubscriptionT, S> where S: ScalarValue, QueryT: GraphQLType, MutationT: GraphQLType, + SubscriptionT: GraphQLType, { type Context = CtxT; type TypeInfo = QueryT::TypeInfo; @@ -75,15 +77,17 @@ where } } -impl<'a, CtxT, S, QueryT, MutationT> crate::GraphQLTypeAsync - for RootNode<'a, QueryT, MutationT, S> +impl<'a, CtxT, S, QueryT, MutationT, SubscriptionT> crate::GraphQLTypeAsync + for RootNode<'a, QueryT, MutationT, SubscriptionT, S> where S: ScalarValue + Send + Sync, QueryT: crate::GraphQLTypeAsync, QueryT::TypeInfo: Send + Sync, MutationT: crate::GraphQLTypeAsync, MutationT::TypeInfo: Send + Sync, - CtxT: Send + Sync, + SubscriptionT: GraphQLType + 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::>() diff --git a/juniper/src/tests/introspection_tests.rs b/juniper/src/tests/introspection_tests.rs index 30b598ce4..26d0a274a 100644 --- a/juniper/src/tests/introspection_tests.rs +++ b/juniper/src/tests/introspection_tests.rs @@ -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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + ); let mut result = crate::introspect(&schema, &database, IntrospectionFormat::WithoutDescriptions).unwrap(); diff --git a/juniper/src/tests/mod.rs b/juniper/src/tests/mod.rs index c2ecd0410..aa5db3a9b 100644 --- a/juniper/src/tests/mod.rs +++ b/juniper/src/tests/mod.rs @@ -9,4 +9,6 @@ pub mod schema; #[cfg(test)] mod schema_introspection; #[cfg(test)] +mod subscriptions; +#[cfg(test)] mod type_info_tests; diff --git a/juniper/src/tests/query_tests.rs b/juniper/src/tests/query_tests.rs index dcc98c57b..301bf9988 100644 --- a/juniper/src/tests/query_tests.rs +++ b/juniper/src/tests/query_tests.rs @@ -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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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::::new()); + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + ); assert_eq!( crate::execute(doc, None, &schema, &Variables::new(), &database).await, diff --git a/juniper/src/tests/subscriptions.rs b/juniper/src/tests/subscriptions.rs new file mode 100644 index 000000000..3ad9a7728 --- /dev/null +++ b/juniper/src/tests/subscriptions.rs @@ -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, MySubscription, DefaultScalarValue>; + +fn run(f: impl std::future::Future) -> O { + let mut rt = tokio::runtime::Runtime::new().unwrap(); + + rt.block_on(f) +} + +type HumanStream = Pin + 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 { + 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, + Vec, ExecutionError>>>, + ), + Vec>, +> { + 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::>()); + 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); +} diff --git a/juniper/src/tests/type_info_tests.rs b/juniper/src/tests/type_info_tests.rs index c3a6d7fa5..87208fd8c 100644 --- a/juniper/src/tests/type_info_tests.rs +++ b/juniper/src/tests/type_info_tests.rs @@ -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(), &()), diff --git a/juniper/src/types/async_await.rs b/juniper/src/types/async_await.rs index d02114b79..ef5f9ec47 100644 --- a/juniper/src/types/async_await.rs +++ b/juniper/src/types/async_await.rs @@ -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: GraphQLType + 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> { 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> where T: GraphQLTypeAsync, @@ -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 where T: GraphQLTypeAsync + 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 diff --git a/juniper/src/types/base.rs b/juniper/src/types/base.rs index 8fa9f02dc..1ce3754f4 100644 --- a/juniper/src/types/base.rs +++ b/juniper/src/types/base.rs @@ -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( instance: &T, info: &T::TypeInfo, @@ -531,6 +533,7 @@ where false } +/// Merges `response_name`/`value` pair into `result` pub(crate) fn merge_key_into(result: &mut Object, response_name: &str, value: Value) { if let Some(&mut (_, ref mut e)) = result .iter_mut() @@ -563,6 +566,7 @@ pub(crate) fn merge_key_into(result: &mut Object, response_name: &str, val result.add_field(response_name, value); } +/// Merges `src` object's fields into `dest` fn merge_maps(dest: &mut Object, src: Object) { for (key, value) in src { if dest.contains_field(&key) { diff --git a/juniper/src/types/containers.rs b/juniper/src/types/containers.rs index 399163d24..e587dee51 100644 --- a/juniper/src/types/containers.rs +++ b/juniper/src/types/containers.rs @@ -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 @@ -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 diff --git a/juniper/src/types/mod.rs b/juniper/src/types/mod.rs index 281adf49b..d8ce957d2 100644 --- a/juniper/src/types/mod.rs +++ b/juniper/src/types/mod.rs @@ -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; diff --git a/juniper/src/types/pointers.rs b/juniper/src/types/pointers.rs index f3c270916..6d32e4ac6 100644 --- a/juniper/src/types/pointers.rs +++ b/juniper/src/types/pointers.rs @@ -219,8 +219,8 @@ impl<'e, S, T> crate::GraphQLTypeAsync for std::sync::Arc where S: ScalarValue + Send + Sync, T: crate::GraphQLTypeAsync, - >::TypeInfo: Sync + Send, - >::Context: Sync + Send, + >::TypeInfo: Send + Sync, + >::Context: Send + Sync, { fn resolve_async<'a>( &'a self, diff --git a/juniper/src/types/scalars.rs b/juniper/src/types/scalars.rs index 3342f050f..c10e71b99 100644 --- a/juniper/src/types/scalars.rs +++ b/juniper/src/types/scalars.rs @@ -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 { + phantom: PhantomData, +} + +// This is safe due to never using `T`. +unsafe impl Send for EmptySubscription {} + +impl EmptySubscription { + /// Construct a new empty subscription + pub fn new() -> Self { + EmptySubscription { + phantom: PhantomData, + } + } +} + +impl GraphQLType for EmptySubscription +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::(&(), &[]).into_meta() + } +} + +impl crate::GraphQLSubscriptionType for EmptySubscription +where + S: ScalarValue + Send + Sync + 'static, + Self: GraphQLType + 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() {} check_if_send::>(); } + + #[test] + fn empty_subscription_is_send() { + fn check_if_send() {} + check_if_send::>(); + } } diff --git a/juniper/src/types/subscriptions.rs b/juniper/src/types/subscriptions.rs new file mode 100644 index 000000000..3635307c6 --- /dev/null +++ b/juniper/src/types/subscriptions.rs @@ -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, + _: &'a CtxT, + ) -> BoxFuture<'a, Result>; +} + +/// 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> {} + +/** + 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: GraphQLType + Send + Sync +where + Self::Context: Send + Sync, + Self::TypeInfo: Send + Sync, + S: ScalarValue + Send + Sync, +{ + /// Resolve into `Value` + /// + /// ## 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>, FieldError>> + 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`, this method resolves each field into + /// `Value>`. + /// + /// 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>, FieldError>> + 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`, this method resolves each fragment into + /// `Value>`. + /// + /// 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>, FieldError>> + 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>> +where + 'inf: 'res, + 'e: 'res, + 'i: 'fut, + 'e: 'fut, + 'ref_e: 'fut, + 'res: 'fut, + T: GraphQLSubscriptionType, + 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> +where + T: GraphQLSubscriptionType + 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> = 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) +} diff --git a/juniper/src/validation/rules/overlapping_fields_can_be_merged.rs b/juniper/src/validation/rules/overlapping_fields_can_be_merged.rs index e10ff3db5..549375513 100644 --- a/juniper/src/validation/rules/overlapping_fields_can_be_merged.rs +++ b/juniper/src/validation/rules/overlapping_fields_can_be_merged.rs @@ -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#" { diff --git a/juniper/src/validation/test_harness.rs b/juniper/src/validation/test_harness.rs index 9aa1d0398..07e87d781 100644 --- a/juniper/src/validation/test_harness.rs +++ b/juniper/src/validation/test_harness.rs @@ -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 +impl GraphQLType 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::(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 where S: ScalarValue + 'a, Q: GraphQLType, M: GraphQLType, + Sub: GraphQLType, 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, M: GraphQLType, + Sub: GraphQLType, 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::::new(), q, factory); if errs.is_empty() { panic!("Expected rule to fail, but no errors were found"); diff --git a/juniper_benchmarks/src/lib.rs b/juniper_benchmarks/src/lib.rs index f6a0330ea..8e1e9652a 100644 --- a/juniper_benchmarks/src/lib.rs +++ b/juniper_benchmarks/src/lib.rs @@ -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 { diff --git a/juniper_codegen/src/impl_object.rs b/juniper_codegen/src/impl_object.rs index f92c8fac0..a2f16cb0e 100644 --- a/juniper_codegen/src/impl_object.rs +++ b/juniper_codegen/src/impl_object.rs @@ -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 } diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 79e7cda05..257367046 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -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::(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 { diff --git a/juniper_codegen/src/util/mod.rs b/juniper_codegen/src/util/mod.rs index f32aec297..3b6d6fb95 100644 --- a/juniper_codegen/src/util/mod.rs +++ b/juniper_codegen/src/util/mod.rs @@ -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::(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, + ) -> #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>, + #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)] diff --git a/juniper_hyper/examples/hyper_server.rs b/juniper_hyper/examples/hyper_server.rs index 134cbc48d..ff6e586e8 100644 --- a/juniper_hyper/examples/hyper_server.rs +++ b/juniper_hyper/examples/hyper_server.rs @@ -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::::new())); + let root_node = Arc::new(RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + )); let new_service = make_service_fn(move |_| { let root_node = root_node.clone(); diff --git a/juniper_hyper/src/lib.rs b/juniper_hyper/src/lib.rs index 896733fb5..64241ac89 100644 --- a/juniper_hyper/src/lib.rs +++ b/juniper_hyper/src/lib.rs @@ -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( - root_node: Arc>, +pub async fn graphql( + root_node: Arc>, context: Arc, request: Request, ) -> Result, hyper::Error> @@ -25,8 +25,10 @@ where CtxT: Send + Sync + 'static, QueryT: GraphQLType + Send + Sync + 'static, MutationT: GraphQLType + Send + Sync + 'static, + SubscrtipionT: GraphQLType + 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( - root_node: Arc>, +pub async fn graphql_async( + root_node: Arc>, context: Arc, request: Request, ) -> Result, hyper::Error> @@ -59,8 +61,10 @@ where CtxT: Send + Sync + 'static, QueryT: GraphQLTypeAsync + Send + Sync + 'static, MutationT: GraphQLTypeAsync + Send + Sync + 'static, + SubscriptionT: GraphQLType + 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, 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 { resp } -async fn execute_request( - root_node: Arc>, +async fn execute_request( + root_node: Arc>, context: Arc, request: GraphQLRequest, ) -> Response @@ -141,8 +146,10 @@ where CtxT: Send + Sync + 'static, QueryT: GraphQLType + Send + Sync + 'static, MutationT: GraphQLType + Send + Sync + 'static, + SubscriptionT: GraphQLType + 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( - root_node: Arc>, +async fn execute_request_async( + root_node: Arc>, context: Arc, request: GraphQLRequest, ) -> Response @@ -169,8 +176,10 @@ where CtxT: Send + Sync + 'static, QueryT: GraphQLTypeAsync + Send + Sync + 'static, MutationT: GraphQLTypeAsync + Send + Sync + 'static, + SubscriptionT: GraphQLType + 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 GraphQLRequest where S: ScalarValue, { - fn execute_sync<'a, CtxT: 'a, QueryT, MutationT>( + fn execute_sync<'a, CtxT: 'a, QueryT, MutationT, SubscriptionT>( self, - root_node: Arc>, + root_node: Arc>, context: Arc, ) -> (bool, hyper::Body) where S: 'a + Send + Sync, QueryT: GraphQLType + 'a, MutationT: GraphQLType + 'a, + SubscriptionT: GraphQLType + '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>, + root_node: Arc>, context: Arc, ) -> (bool, hyper::Body) where S: Send + Sync, QueryT: GraphQLTypeAsync + Send + Sync, MutationT: GraphQLTypeAsync + Send + Sync, + SubscriptionT: GraphQLType + 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::::new())); + let root_node = Arc::new(RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + )); let new_service = make_service_fn(move |_| { let root_node = root_node.clone(); diff --git a/juniper_iron/examples/iron_server.rs b/juniper_iron/examples/iron_server.rs index 2c6e7f9b7..20cc5dee2 100644 --- a/juniper_iron/examples/iron_server.rs +++ b/juniper_iron/examples/iron_server.rs @@ -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 { fn main() { let mut mount = Mount::new(); - let graphql_endpoint = - GraphQLHandler::new(context_factory, Query, EmptyMutation::::new()); + let graphql_endpoint = GraphQLHandler::new( + context_factory, + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + ); let graphiql_endpoint = GraphiQLHandler::new("/graphql"); mount.mount("/", graphiql_endpoint); diff --git a/juniper_iron/src/lib.rs b/juniper_iron/src/lib.rs index 1479a6ed7..9aea2b933 100644 --- a/juniper_iron/src/lib.rs +++ b/juniper_iron/src/lib.rs @@ -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::::new()); + context_factory, + QueryRoot, + EmptyMutation::::new(), + EmptySubscription::::new(), + ); // Start serving the schema at the root on port 8080. Iron::new(graphql_endpoint).http("localhost:8080").unwrap(); @@ -146,14 +150,15 @@ impl GraphQLBatchRequest 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, + root_node: &'a RootNode, context: &CtxT, ) -> GraphQLBatchResponse<'a, S> where QueryT: GraphQLType, MutationT: GraphQLType, + Subscription: GraphQLType, { 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 + Send + Sync + 'static, CtxT: 'static, Query: GraphQLType + Send + Sync + 'static, Mutation: GraphQLType + Send + Sync + 'static, + Subscription: GraphQLType + 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 + Send + Sync + 'static, CtxT: 'static, Query: GraphQLType + Send + Sync + 'static, Mutation: GraphQLType + Send + Sync + 'static, + Subscription: GraphQLType + 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 + Send + Sync + 'static, CtxT: 'static, Query: GraphQLType + Send + Sync + 'static, Mutation: GraphQLType + Send + Sync + 'static, + Subscription: GraphQLType + Send + Sync + 'static, 'a: 'static, { fn handle(&self, mut req: &mut Request) -> IronResult { @@ -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::::new(), + EmptySubscription::::new(), )) } } diff --git a/juniper_rocket/examples/rocket_server.rs b/juniper_rocket/examples/rocket_server.rs index 096b98cd5..f352c36e2 100644 --- a/juniper_rocket/examples/rocket_server.rs +++ b/juniper_rocket/examples/rocket_server.rs @@ -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>; +type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>; #[rocket::get("/")] fn graphiql() -> content::Html { @@ -35,7 +35,11 @@ fn post_graphql_handler( fn main() { rocket::ignite() .manage(Database::new()) - .manage(Schema::new(Query, EmptyMutation::::new())) + .manage(Schema::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + )) .mount( "/", rocket::routes![graphiql, get_graphql_handler, post_graphql_handler], diff --git a/juniper_rocket/src/lib.rs b/juniper_rocket/src/lib.rs index fde19fc03..93cffe28a 100644 --- a/juniper_rocket/src/lib.rs +++ b/juniper_rocket/src/lib.rs @@ -82,14 +82,15 @@ impl GraphQLBatchRequest 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, + root_node: &'a RootNode, context: &CtxT, ) -> GraphQLBatchResponse<'a, S> where QueryT: GraphQLType, MutationT: GraphQLType, + SubscriptionT: GraphQLType, { match *self { GraphQLBatchRequest::Single(ref request) => { @@ -150,6 +151,7 @@ pub fn graphiql_source(graphql_endpoint_url: &str) -> content::Html { pub fn playground_source(graphql_endpoint_url: &str) -> content::Html { 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( + pub fn execute_sync( &self, - root_node: &RootNode, + root_node: &RootNode, context: &CtxT, ) -> GraphQLResponse where QueryT: GraphQLType, MutationT: GraphQLType, + SubscriptionT: GraphQLType, { 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>; + /// # type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>; /// # /// #[rocket::get("/graphql?")] /// 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>; + type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>; #[get("/?")] 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::::new())) + rocket::ignite().manage(Database::new()).manage(Schema::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + )) } fn make_test_response(request: &LocalRequest) -> http_tests::TestResponse { diff --git a/juniper_subscriptions/.gitignore b/juniper_subscriptions/.gitignore new file mode 100644 index 000000000..0d722487a --- /dev/null +++ b/juniper_subscriptions/.gitignore @@ -0,0 +1,4 @@ +/target +/examples/**/target/**/* +**/*.rs.bk +Cargo.lock diff --git a/juniper_subscriptions/CHANGELOG.md b/juniper_subscriptions/CHANGELOG.md new file mode 100644 index 000000000..052324725 --- /dev/null +++ b/juniper_subscriptions/CHANGELOG.md @@ -0,0 +1,3 @@ +# master + +- Initial Release diff --git a/juniper_subscriptions/Cargo.toml b/juniper_subscriptions/Cargo.toml new file mode 100644 index 000000000..27ccddd51 --- /dev/null +++ b/juniper_subscriptions/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "juniper_subscriptions" +version = "0.14.2" +authors = ["nWacky "] +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"] } diff --git a/juniper_subscriptions/LICENSE b/juniper_subscriptions/LICENSE new file mode 100644 index 000000000..177ee843c --- /dev/null +++ b/juniper_subscriptions/LICENSE @@ -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. diff --git a/juniper_subscriptions/Makefile.toml b/juniper_subscriptions/Makefile.toml new file mode 100644 index 000000000..16c540d43 --- /dev/null +++ b/juniper_subscriptions/Makefile.toml @@ -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 diff --git a/juniper_subscriptions/README.md b/juniper_subscriptions/README.md new file mode 100644 index 000000000..4c8e68595 --- /dev/null +++ b/juniper_subscriptions/README.md @@ -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 diff --git a/juniper_subscriptions/release.toml b/juniper_subscriptions/release.toml new file mode 100644 index 000000000..1c42935a7 --- /dev/null +++ b/juniper_subscriptions/release.toml @@ -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}}"}, +] diff --git a/juniper_subscriptions/src/lib.rs b/juniper_subscriptions/src/lib.rs new file mode 100644 index 000000000..a582b8b44 --- /dev/null +++ b/juniper_subscriptions/src/lib.rs @@ -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 + Send + Sync, + QueryT::TypeInfo: Send + Sync, + MutationT: GraphQLTypeAsync + Send + Sync, + MutationT::TypeInfo: Send + Sync, + SubscriptionT: GraphQLSubscriptionType + 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 + Send + Sync, + QueryT::TypeInfo: Send + Sync, + MutationT: GraphQLTypeAsync + Send + Sync, + MutationT::TypeInfo: Send + Sync, + SubscriptionT: GraphQLSubscriptionType + 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 + Send + Sync, + QueryT::TypeInfo: Send + Sync, + MutationT: GraphQLTypeAsync + Send + Sync, + MutationT::TypeInfo: Send + Sync, + SubscriptionT: GraphQLSubscriptionType + 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, + context: &'a CtxT, + ) -> BoxFuture<'a, Result> { + 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` into `Stream` 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>` - returns [`Value::Null`] if [`Value::Object`] consists of sub-objects +pub struct Connection<'a, S> { + stream: Pin> + 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>, errors: Vec>) -> 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> { + // 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>` - returns [`Value::Null`] if [`Value::Object`] consists of sub-objects +fn whole_responses_stream<'a, S>( + stream: Value>, + errors: Vec>, +) -> Pin> + 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>> { + 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` 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::>(); + 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::::error( + FieldError::new("field error", Value::Null), + )]; + let expected = serde_json::to_string(&expected).unwrap(); + + let result = whole_responses_stream::( + Value::Null, + vec![ExecutionError::at_origin(FieldError::new( + "field error", + Value::Null, + ))], + ) + .collect::>() + .await; + let result = serde_json::to_string(&result).unwrap(); + + assert_eq!(result, expected); + } + + #[tokio::test] + async fn value_null() { + let expected = vec![GraphQLResponse::::from_result(Ok(( + Value::Null, + vec![], + )))]; + let expected = serde_json::to_string(&expected).unwrap(); + + let result = whole_responses_stream::(Value::Null, vec![]) + .collect::>() + .await; + let result = serde_json::to_string(&result).unwrap(); + + assert_eq!(result, expected); + } + + type PollResult = Result, ExecutionError>; + + #[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> { + if counter == 5 { + return Poll::Ready(None); + } + counter += 1; + Poll::Ready(Some(Ok(Value::Scalar(DefaultScalarValue::Int(counter))))) + }); + + let result = + whole_responses_stream::(Value::Scalar(Box::pin(stream)), vec![]) + .collect::>() + .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> = 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::(Value::List(streams), vec![]) + .collect::>() + .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> { + 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> { + if counter == 2 { + return Poll::Ready(None); + } + counter += 1; + Poll::Ready(Some(Ok(Value::Scalar(DefaultScalarValue::Int(counter))))) + }); + + let vals: Vec<(&str, Value)> = vec![ + ("one", Value::Scalar(Box::pin(big_stream))), + ("two", Value::Scalar(Box::pin(small_stream))), + ]; + + let result = whole_responses_stream::( + Value::Object(Object::from_iter(vals.into_iter())), + vec![], + ) + .collect::>() + .await; + let result = serde_json::to_string(&result).unwrap(); + + assert_eq!(result, expected); + } +} diff --git a/juniper_warp/CHANGELOG.md b/juniper_warp/CHANGELOG.md index dca5b7183..7ce363ffd 100644 --- a/juniper_warp/CHANGELOG.md +++ b/juniper_warp/CHANGELOG.md @@ -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`. diff --git a/juniper_warp/Cargo.toml b/juniper_warp/Cargo.toml index 4f38a248a..24d20c5c2 100644 --- a/juniper_warp/Cargo.toml +++ b/juniper_warp/Cargo.toml @@ -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"] } diff --git a/juniper_warp/examples/warp_server.rs b/juniper_warp/examples/warp_server.rs index 09bf6010a..a6eb52174 100644 --- a/juniper_warp/examples/warp_server.rs +++ b/juniper_warp/examples/warp_server.rs @@ -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>; +type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>; fn schema() -> Schema { - Schema::new(Query, EmptyMutation::::new()) + Schema::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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 } diff --git a/juniper_warp/src/lib.rs b/juniper_warp/src/lib.rs index 6b3a0b001..769062c47 100644 --- a/juniper_warp/src/lib.rs +++ b/juniper_warp/src/lib.rs @@ -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 GraphQLBatchRequest 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, + root_node: &'a juniper::RootNode, context: &CtxT, ) -> GraphQLBatchResponse<'a, S> where QueryT: juniper::GraphQLType, MutationT: juniper::GraphQLType, + SubscriptionT: juniper::GraphQLType, + 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 + Send + Sync, MutationT::TypeInfo: Send + Sync, + SubscriptionT: juniper::GraphQLSubscriptionType + 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::>(); - 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,91 +200,89 @@ 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( - schema: juniper::RootNode<'static, Query, Mutation, S>, +pub fn make_graphql_filter( + schema: juniper::RootNode<'static, Query, Mutation, Subscription, S>, context_extractor: BoxedFilter<(Context,)>, ) -> BoxedFilter<(warp::http::Response>,)> where S: ScalarValue + Send + Sync + 'static, - Context: Send + 'static, - Query: juniper::GraphQLType + Send + Sync + 'static, - Mutation: juniper::GraphQLType + Send + Sync + 'static, + Context: Send + Sync + 'static, + Query: juniper::GraphQLTypeAsync + Send + Sync + 'static, + Query::TypeInfo: Send + Sync, + Mutation: juniper::GraphQLTypeAsync + Send + Sync + 'static, + Mutation::TypeInfo: Send + Sync, + Subscription: juniper::GraphQLSubscriptionType + 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| -> 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 handle_post_request = move |context: Context, request: GraphQLBatchRequest| { + 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::post2() + 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| - -> 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 handle_get_request = + move |context: Context, mut request: std::collections::HashMap| { + let schema = schema.clone(); - 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, - ); + async move { + let variables = match request.remove("variables") { + None => None, + Some(vs) => serde_json::from_str(&vs)?, + }; - 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 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; - let get_filter = warp::get2() - .and(context_extractor) + 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() } -/// FIXME: docs -pub fn make_graphql_filter_async( - schema: juniper::RootNode<'static, Query, Mutation, S>, +/// Make a synchronous filter for graphql endpoint. +pub fn make_graphql_filter_sync( + schema: juniper::RootNode<'static, Query, Mutation, Subscription, S>, context_extractor: BoxedFilter<(Context,)>, ) -> BoxedFilter<(warp::http::Response>,)> where S: ScalarValue + Send + Sync + 'static, Context: Send + Sync + 'static, - Query: juniper::GraphQLTypeAsync + Send + Sync + 'static, - Query::TypeInfo: Send + Sync, - Mutation: juniper::GraphQLTypeAsync + Send + Sync + 'static, - Mutation::TypeInfo: Send + Sync, + Query: juniper::GraphQLType + Send + Sync + 'static, + Mutation: juniper::GraphQLType + Send + Sync + 'static, + Subscription: juniper::GraphQLType + Send + Sync + 'static, { let schema = Arc::new(schema); let post_schema = schema.clone(); @@ -289,19 +291,21 @@ where move |context: Context, request: GraphQLBatchRequest| -> 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| -> 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, bool), failure::Error>, ) -> warp::http::Response> { @@ -359,8 +380,9 @@ fn build_response( } } -type Response = - Box>, Error = warp::reject::Rejection> + Send>; +type Response = Pin< + Box>, 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>,)> { 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> { +fn playground_response( + graphql_endpoint_url: &'static str, + subscriptions_endpoint_url: Option<&'static str>, +) -> warp::http::Response> { 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( + websocket: warp::ws::WebSocket, + coordinator: Arc>, + context: Context, + ) -> impl Future + Send + where + S: ScalarValue + Send + Sync + 'static, + Context: Clone + Send + Sync + 'static, + Query: juniper::GraphQLTypeAsync + Send + Sync + 'static, + Query::TypeInfo: Send + Sync, + Mutation: juniper::GraphQLTypeAsync + Send + Sync + 'static, + Mutation::TypeInfo: Send + Sync, + Subscription: + juniper::GraphQLSubscriptionType + 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 = 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::::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: Deserialize<'de>")] + struct WsPayload + where + S: ScalarValue + Send + Sync + 'static, + { + id: Option, + #[serde(rename(deserialize = "type"))] + type_name: String, + payload: Option>, + } + + #[derive(Debug, Deserialize)] + #[serde(bound = "InputValue: Deserialize<'de>")] + struct GraphQLPayload + where + S: ScalarValue + Send + Sync + 'static, + { + variables: Option>, + extensions: Option>, + #[serde(rename(deserialize = "operationName"))] + operaton_name: Option, + query: Option, + } + + #[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("")); } - #[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>; + type Schema = + juniper::RootNode<'static, Query, EmptyMutation, EmptySubscription>; - let schema: Schema = RootNode::new(Query, EmptyMutation::::new()); + let schema: Schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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>; + type Schema = + juniper::RootNode<'static, Query, EmptyMutation, EmptySubscription>; - let schema: Schema = RootNode::new(Query, EmptyMutation::::new()); + let schema: Schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::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>; - - fn warp_server() -> warp::filters::BoxedFilter<(warp::http::Response>,)> { - let schema: Schema = RootNode::new(Query, EmptyMutation::::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>,)>, - } - - // 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>) -> 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::>() - .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, EmptySubscription>; +// +// fn warp_server() -> warp::filters::BoxedFilter<(warp::http::Response>,)> { +// let schema: Schema = RootNode::new( +// Query, +// EmptyMutation::::new(), +// EmptySubscription::::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>,)>, +// } +// +// // 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>) -> 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::>() +// .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); +// } +//}