Add integration crate for the warp framework (#216)
This commit is contained in:
parent
1ee3dab3b7
commit
fd636e07df
9 changed files with 641 additions and 2 deletions
|
@ -21,6 +21,14 @@ script:
|
|||
- wget -O ~/.cargo/bin/cargo-make https://bintray.com/sagiegurari/cargo-make/download_file?file_path=cargo-make_v0.11.0u
|
||||
- chmod 777 ~/.cargo/bin/cargo-make
|
||||
- cargo make workspace-ci-flow --no-workspace
|
||||
# The tests for juniper_warp need to run separately because warp has higher
|
||||
# minimum rust version requirements than juniper.
|
||||
#
|
||||
# There is a cargo-make issue about this problem: https://github.com/sagiegurari/cargo-make/issues/110
|
||||
#
|
||||
# We check for '1' because we the only channels we want to support are
|
||||
# "stable", "beta" and "nightly" (that will be the values of $TRAVIS_RUST_VERSION)
|
||||
- if ! [[ $TRAVIS_RUST_VERSION = *'1'* ]]; then cargo test --all --manifest-path=juniper_warp/Cargo.toml; fi
|
||||
|
||||
before_deploy:
|
||||
- rm -rf target/package/
|
||||
|
|
|
@ -43,7 +43,7 @@ For specific information about macros, types and the Juniper api, the
|
|||
You can also check out [src/tests/schema.rs][test_schema_rs] to see a complex
|
||||
schema including polymorphism with traits and interfaces.
|
||||
For an example of web framework integration,
|
||||
see the [hyper][hyper_examples], [rocket][rocket_examples], and [iron][iron_examples] examples folders.
|
||||
see the [hyper][hyper_examples], [rocket][rocket_examples], [iron][iron_examples], and [warp][warp_examples] examples folders.
|
||||
|
||||
|
||||
## Features
|
||||
|
@ -75,6 +75,7 @@ your Schemas automatically.
|
|||
* [hyper][hyper]
|
||||
* [rocket][rocket]
|
||||
* [iron][iron]
|
||||
* [warp][warp]
|
||||
|
||||
## Guides & Examples
|
||||
|
||||
|
@ -99,8 +100,9 @@ Juniper has not reached 1.0 yet, thus some API instability should be expected.
|
|||
[book]: https://graphql-rust.github.io
|
||||
[book_quickstart]: https://graphql-rust.github.io/quickstart.html
|
||||
[docsrs]: https://docs.rs/juniper
|
||||
[warp]: https://github.com/seanmonstar/warp
|
||||
[warp_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_warp/examples
|
||||
|
||||
[uuid]: https://crates.io/crates/uuid
|
||||
[url]: https://crates.io/crates/url
|
||||
[chrono]: https://crates.io/crates/chrono
|
||||
|
||||
|
|
|
@ -137,6 +137,7 @@ pub mod tests {
|
|||
|
||||
/// Normalized response content we expect to get back from
|
||||
/// the http framework integration we are testing.
|
||||
#[derive(Debug)]
|
||||
pub struct TestResponse {
|
||||
pub status_code: i32,
|
||||
pub body: Option<String>,
|
||||
|
|
4
juniper_warp/.gitignore
vendored
Normal file
4
juniper_warp/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
/target
|
||||
/examples/**/target/**/*
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
24
juniper_warp/Cargo.toml
Normal file
24
juniper_warp/Cargo.toml
Normal file
|
@ -0,0 +1,24 @@
|
|||
[package]
|
||||
name = "juniper_warp"
|
||||
version = "0.1.0"
|
||||
authors = ["Tom Houlé <tom@tomhoule.com>"]
|
||||
description = "Juniper GraphQL integration with Warp"
|
||||
documentation = "https://docs.rs/juniper_warp"
|
||||
repository = "https://github.com/graphql-rust/juniper"
|
||||
|
||||
[dependencies]
|
||||
warp = "0.1.2"
|
||||
juniper = { path = "../juniper", version = "0.9.2", default-features = false }
|
||||
serde_json = "1.0.24"
|
||||
serde_derive = "1.0.75"
|
||||
failure = "0.1.2"
|
||||
futures-cpupool = "0.1.8"
|
||||
futures = "0.1.23"
|
||||
serde = "1.0.75"
|
||||
|
||||
[dev-dependencies]
|
||||
juniper = { path = "../juniper", version = "0.9.2", features = ["expose-test-schema", "serde_json"] }
|
||||
percent-encoding = "1.0"
|
||||
|
||||
[workspace]
|
||||
members = [".", "examples/warp_server"]
|
33
juniper_warp/README.md
Normal file
33
juniper_warp/README.md
Normal file
|
@ -0,0 +1,33 @@
|
|||
# juniper_warp
|
||||
|
||||
This repository contains the [warp][warp] web server integration for
|
||||
[Juniper][Juniper], a [GraphQL][GraphQL] implementation for Rust.
|
||||
|
||||
## Documentation
|
||||
|
||||
For documentation, including guides and examples, check out [Juniper][Juniper].
|
||||
|
||||
A basic usage example can also be found in the [API documentation][documentation].
|
||||
|
||||
## Examples
|
||||
|
||||
Check [examples/warp_server][example] for example code of a working warp
|
||||
server with GraphQL handlers.
|
||||
|
||||
## Links
|
||||
|
||||
* [Juniper][Juniper]
|
||||
* [API Reference][documetation]
|
||||
* [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
|
||||
[GraphQL]: http://graphql.org
|
||||
[documentation]: https://docs.rs/juniper_warp
|
||||
[example]: https://github.com/graphql-rust/juniper_warp/blob/master/examples/warp_server
|
11
juniper_warp/examples/warp_server/Cargo.toml
Normal file
11
juniper_warp/examples/warp_server/Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "warp_server"
|
||||
version = "0.1.0"
|
||||
authors = ["Tom Houlé <tom@tomhoule.com>"]
|
||||
|
||||
[dependencies]
|
||||
warp = "0.1.0"
|
||||
juniper_warp = { path = "../.." }
|
||||
env_logger = "0.5.11"
|
||||
log = "0.4.3"
|
||||
juniper = { path = "../../../juniper", version = "0.9.2", features = ["expose-test-schema", "serde_json"] }
|
47
juniper_warp/examples/warp_server/src/main.rs
Normal file
47
juniper_warp/examples/warp_server/src/main.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
#![deny(warnings)]
|
||||
|
||||
extern crate env_logger;
|
||||
#[macro_use]
|
||||
extern crate log as irrelevant_log;
|
||||
extern crate juniper;
|
||||
extern crate juniper_warp;
|
||||
extern crate warp;
|
||||
|
||||
use juniper::tests::model::Database;
|
||||
use juniper::{EmptyMutation, RootNode};
|
||||
use warp::{http::Response, log, Filter};
|
||||
|
||||
type Schema = RootNode<'static, Database, EmptyMutation<Database>>;
|
||||
|
||||
fn schema() -> Schema {
|
||||
Schema::new(Database::new(), EmptyMutation::<Database>::new())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
::std::env::set_var("RUST_LOG", "warp_server");
|
||||
env_logger::init();
|
||||
|
||||
let log = log("warp_server");
|
||||
|
||||
let homepage = warp::index().map(|| {
|
||||
Response::builder()
|
||||
.header("content-type", "text/html")
|
||||
.body(format!(
|
||||
"<html><h1>juniper_warp</h1><div>visit <a href=\"/graphiql\">/graphiql</a></html>"
|
||||
))
|
||||
});
|
||||
|
||||
info!("Listening on 127.0.0.1:8080");
|
||||
|
||||
let state = warp::any().map(move || Database::new());
|
||||
let graphql_filter = juniper_warp::make_graphql_filter(schema(), state.boxed());
|
||||
|
||||
warp::serve(
|
||||
warp::get2()
|
||||
.and(warp::path("graphiql"))
|
||||
.and(juniper_warp::graphiql_handler("/graphql"))
|
||||
.or(homepage)
|
||||
.or(warp::path("graphql").and(graphql_filter))
|
||||
.with(log),
|
||||
).run(([127, 0, 0, 1], 8080));
|
||||
}
|
509
juniper_warp/src/lib.rs
Normal file
509
juniper_warp/src/lib.rs
Normal file
|
@ -0,0 +1,509 @@
|
|||
/*!
|
||||
|
||||
# juniper_warp
|
||||
|
||||
This repository contains the [warp][warp] web server integration for
|
||||
[Juniper][Juniper], a [GraphQL][GraphQL] implementation for Rust.
|
||||
|
||||
## Documentation
|
||||
|
||||
For documentation, including guides and examples, check out [Juniper][Juniper].
|
||||
|
||||
A basic usage example can also be found in the [Api documentation][documentation].
|
||||
|
||||
## Examples
|
||||
|
||||
Check [examples/warp_server][example] for example code of a working warp
|
||||
server with GraphQL 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
|
||||
[GraphQL]: http://graphql.org
|
||||
[documentation]: https://docs.rs/juniper_warp
|
||||
[example]: https://github.com/graphql-rust/juniper_warp/blob/master/examples/warp_server
|
||||
|
||||
*/
|
||||
|
||||
#![deny(missing_docs)]
|
||||
#![deny(warnings)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate failure;
|
||||
extern crate futures;
|
||||
extern crate futures_cpupool;
|
||||
extern crate juniper;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
extern crate warp;
|
||||
|
||||
#[cfg(test)]
|
||||
extern crate percent_encoding;
|
||||
|
||||
use futures::Future;
|
||||
use futures_cpupool::CpuPool;
|
||||
use std::sync::Arc;
|
||||
use warp::{filters::BoxedFilter, Filter};
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
enum GraphQLBatchRequest {
|
||||
Single(juniper::http::GraphQLRequest),
|
||||
Batch(Vec<juniper::http::GraphQLRequest>),
|
||||
}
|
||||
|
||||
impl GraphQLBatchRequest {
|
||||
pub fn execute<'a, CtxT, QueryT, MutationT>(
|
||||
&'a self,
|
||||
root_node: &juniper::RootNode<QueryT, MutationT>,
|
||||
context: &CtxT,
|
||||
) -> GraphQLBatchResponse<'a>
|
||||
where
|
||||
QueryT: juniper::GraphQLType<Context = CtxT>,
|
||||
MutationT: juniper::GraphQLType<Context = CtxT>,
|
||||
{
|
||||
match self {
|
||||
&GraphQLBatchRequest::Single(ref request) => {
|
||||
GraphQLBatchResponse::Single(request.execute(root_node, context))
|
||||
}
|
||||
&GraphQLBatchRequest::Batch(ref requests) => GraphQLBatchResponse::Batch(
|
||||
requests
|
||||
.iter()
|
||||
.map(|request| request.execute(root_node, context))
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(untagged)]
|
||||
enum GraphQLBatchResponse<'a> {
|
||||
Single(juniper::http::GraphQLResponse<'a>),
|
||||
Batch(Vec<juniper::http::GraphQLResponse<'a>>),
|
||||
}
|
||||
|
||||
impl<'a> GraphQLBatchResponse<'a> {
|
||||
fn is_ok(&self) -> bool {
|
||||
match self {
|
||||
GraphQLBatchResponse::Single(res) => res.is_ok(),
|
||||
GraphQLBatchResponse::Batch(reses) => reses.iter().all(|res| res.is_ok()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Make a filter for graphql endpoint.
|
||||
///
|
||||
/// The `schema` argument is your juniper schema.
|
||||
///
|
||||
/// The `context_extractor` argument should be a filter that provides the GraphQL context required by the schema.
|
||||
///
|
||||
/// In order to avoid blocking, this helper will create a [CpuPool](../futures_cpupool/struct.CpuPool.html) to resolve GraphQL requests.
|
||||
///
|
||||
/// If you want to pass your own threadpool, use [make_graphql_filter_with_thread_pool](fn.make_graphql_filter_with_thread_pool.html) instead.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate juniper_warp;
|
||||
/// # #[macro_use]
|
||||
/// # extern crate juniper;
|
||||
/// # extern crate warp;
|
||||
/// #
|
||||
/// # use std::sync::Arc;
|
||||
/// # use warp::Filter;
|
||||
/// # use juniper::{EmptyMutation, RootNode};
|
||||
/// # use juniper_warp::make_graphql_filter;
|
||||
/// #
|
||||
/// # fn main() {
|
||||
/// type UserId = String;
|
||||
/// # #[derive(Debug)]
|
||||
/// struct AppState(Vec<i64>);
|
||||
/// struct ExampleContext(Arc<AppState>, UserId);
|
||||
///
|
||||
/// struct QueryRoot;
|
||||
///
|
||||
/// graphql_object! (QueryRoot: ExampleContext |&self| {
|
||||
/// field say_hello(&executor) -> String {
|
||||
/// let context = executor.context();
|
||||
///
|
||||
/// format!("good morning {}, the app state is {:?}", context.1, context.0)
|
||||
/// }
|
||||
/// });
|
||||
///
|
||||
/// let schema = RootNode::new(QueryRoot, EmptyMutation::new());
|
||||
///
|
||||
/// let app_state = Arc::new(AppState(vec![3, 4, 5]));
|
||||
/// let app_state = warp::any().map(move || app_state.clone());
|
||||
///
|
||||
/// let context_extractor = warp::any()
|
||||
/// .and(warp::header::<String>("authorization"))
|
||||
/// .and(app_state)
|
||||
/// .map(|auth_header: String, app_state: Arc<AppState>| {
|
||||
/// let user_id = auth_header; // we believe them
|
||||
/// ExampleContext(app_state, user_id)
|
||||
/// })
|
||||
/// .boxed();
|
||||
///
|
||||
/// let graphql_filter = make_graphql_filter(schema, context_extractor);
|
||||
///
|
||||
/// let graphql_endpoint = warp::path("graphql")
|
||||
/// .and(warp::post2())
|
||||
/// .and(graphql_filter);
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn make_graphql_filter<Query, Mutation, Context>(
|
||||
schema: juniper::RootNode<'static, Query, Mutation>,
|
||||
context_extractor: BoxedFilter<(Context,)>,
|
||||
) -> BoxedFilter<(warp::http::Response<Vec<u8>>,)>
|
||||
where
|
||||
Context: Send + 'static,
|
||||
Query: juniper::GraphQLType<Context = Context, TypeInfo = ()> + Send + Sync + 'static,
|
||||
Mutation: juniper::GraphQLType<Context = Context, TypeInfo = ()> + Send + Sync + 'static,
|
||||
{
|
||||
let pool = CpuPool::new_num_cpus();
|
||||
make_graphql_filter_with_thread_pool(schema, context_extractor, pool)
|
||||
}
|
||||
|
||||
type Response =
|
||||
Box<Future<Item = warp::http::Response<Vec<u8>>, Error = warp::reject::Rejection> + Send>;
|
||||
|
||||
/// Same as [make_graphql_filter](./fn.make_graphql_filter.html), but use the provided [CpuPool](../futures_cpupool/struct.CpuPool.html) instead.
|
||||
pub fn make_graphql_filter_with_thread_pool<Query, Mutation, Context>(
|
||||
schema: juniper::RootNode<'static, Query, Mutation>,
|
||||
context_extractor: BoxedFilter<(Context,)>,
|
||||
thread_pool: futures_cpupool::CpuPool,
|
||||
) -> BoxedFilter<(warp::http::Response<Vec<u8>>,)>
|
||||
where
|
||||
Context: Send + 'static,
|
||||
Query: juniper::GraphQLType<Context = Context, TypeInfo = ()> + Send + Sync + 'static,
|
||||
Mutation: juniper::GraphQLType<Context = Context, TypeInfo = ()> + Send + Sync + 'static,
|
||||
{
|
||||
let schema = Arc::new(schema);
|
||||
let post_schema = schema.clone();
|
||||
let pool_filter = warp::any().map(move || thread_pool.clone());
|
||||
|
||||
let handle_post_request =
|
||||
move |context: Context, request: GraphQLBatchRequest, pool: CpuPool| -> Response {
|
||||
let schema = post_schema.clone();
|
||||
Box::new(
|
||||
pool.spawn_fn(move || {
|
||||
let response = request.execute(&schema, &context);
|
||||
Ok((serde_json::to_vec(&response)?, response.is_ok()))
|
||||
}).then(|result| ::futures::future::done(Ok(build_response(result))))
|
||||
.map_err(|_: failure::Error| warp::reject::server_error()),
|
||||
)
|
||||
};
|
||||
|
||||
let post_filter = warp::post2()
|
||||
.and(context_extractor.clone())
|
||||
.and(warp::body::json())
|
||||
.and(pool_filter.clone())
|
||||
.and_then(handle_post_request);
|
||||
|
||||
let handle_get_request = move |context: Context,
|
||||
mut request: std::collections::HashMap<String, String>,
|
||||
pool: CpuPool|
|
||||
-> Response {
|
||||
let schema = schema.clone();
|
||||
Box::new(
|
||||
pool.spawn_fn(move || {
|
||||
let variables = match request.remove("variables") {
|
||||
None => None,
|
||||
Some(vs) => serde_json::from_str(&vs)?,
|
||||
};
|
||||
|
||||
let graphql_request = juniper::http::GraphQLRequest::new(
|
||||
request.remove("query").ok_or_else(|| {
|
||||
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);
|
||||
Ok((serde_json::to_vec(&response)?, response.is_ok()))
|
||||
}).then(|result| ::futures::future::done(Ok(build_response(result))))
|
||||
.map_err(|_: failure::Error| warp::reject::server_error()),
|
||||
)
|
||||
};
|
||||
|
||||
let get_filter = warp::get2()
|
||||
.and(context_extractor.clone())
|
||||
.and(warp::filters::query::query())
|
||||
.and(pool_filter)
|
||||
.and_then(handle_get_request);
|
||||
|
||||
get_filter.or(post_filter).unify().boxed()
|
||||
}
|
||||
|
||||
fn build_response(
|
||||
response: Result<(Vec<u8>, bool), failure::Error>,
|
||||
) -> warp::http::Response<Vec<u8>> {
|
||||
match response {
|
||||
Ok((body, is_ok)) => warp::http::Response::builder()
|
||||
.status(if is_ok { 200 } else { 400 })
|
||||
.header("content-type", "application/json")
|
||||
.body(body)
|
||||
.expect("response is valid"),
|
||||
Err(_) => warp::http::Response::builder()
|
||||
.status(warp::http::StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(Vec::new())
|
||||
.expect("status code is valid"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a filter that replies with an HTML page containing GraphiQL. This does not handle routing, so you can mount it on any endpoint.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate warp;
|
||||
/// # extern crate juniper_warp;
|
||||
/// #
|
||||
/// # use warp::Filter;
|
||||
/// # use juniper_warp::graphiql_handler;
|
||||
/// #
|
||||
/// # fn main() {
|
||||
/// let graphiql_route = warp::path("graphiql").and(graphiql_handler("/graphql"));
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn graphiql_handler(
|
||||
graphql_endpoint_url: &'static str,
|
||||
) -> warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)> {
|
||||
warp::any()
|
||||
.map(move || graphiql_response(graphql_endpoint_url))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn graphiql_response(graphql_endpoint_url: &'static str) -> warp::http::Response<Vec<u8>> {
|
||||
warp::http::Response::builder()
|
||||
.header("content-type", "text/html;charset=utf-8")
|
||||
.body(juniper::graphiql::graphiql_source(graphql_endpoint_url).into_bytes())
|
||||
.expect("response is valid")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use warp::http;
|
||||
use warp::test::request;
|
||||
|
||||
#[test]
|
||||
fn graphiql_response_does_not_panic() {
|
||||
graphiql_response("/abcd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graphiql_endpoint_matches() {
|
||||
let filter = warp::get2()
|
||||
.and(warp::path("graphiql"))
|
||||
.and(graphiql_handler("/graphql"));
|
||||
let result = request()
|
||||
.method("GET")
|
||||
.path("/graphiql")
|
||||
.header("accept", "text/html")
|
||||
.filter(&filter);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graphiql_endpoint_returns_graphiql_source() {
|
||||
let filter = warp::get2()
|
||||
.and(warp::path("dogs-api"))
|
||||
.and(warp::path("graphiql"))
|
||||
.and(graphiql_handler("/dogs-api/graphql"));
|
||||
let response = request()
|
||||
.method("GET")
|
||||
.path("/dogs-api/graphiql")
|
||||
.header("accept", "text/html")
|
||||
.reply(&filter);
|
||||
|
||||
assert_eq!(response.status(), http::StatusCode::OK);
|
||||
assert_eq!(
|
||||
response.headers().get("content-type").unwrap(),
|
||||
"text/html;charset=utf-8"
|
||||
);
|
||||
let body = String::from_utf8(response.body().to_vec()).unwrap();
|
||||
|
||||
assert!(body.contains("<script>var GRAPHQL_URL = '/dogs-api/graphql';</script>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graphql_handler_works_json_post() {
|
||||
use juniper::tests::model::Database;
|
||||
use juniper::{EmptyMutation, RootNode};
|
||||
|
||||
type Schema = juniper::RootNode<'static, Database, EmptyMutation<Database>>;
|
||||
|
||||
let schema: Schema = RootNode::new(Database::new(), EmptyMutation::<Database>::new());
|
||||
|
||||
let state = warp::any().map(move || Database::new());
|
||||
let filter = warp::path("graphql2").and(make_graphql_filter(schema, state.boxed()));
|
||||
|
||||
let response = request()
|
||||
.method("POST")
|
||||
.path("/graphql2")
|
||||
.header("accept", "application/json")
|
||||
.header("content-type", "application/json")
|
||||
.body(r##"{ "variables": null, "query": "{ hero(episode: NEW_HOPE) { name } }" }"##)
|
||||
.reply(&filter);
|
||||
|
||||
assert_eq!(response.status(), http::StatusCode::OK);
|
||||
assert_eq!(
|
||||
response.headers().get("content-type").unwrap(),
|
||||
"application/json",
|
||||
);
|
||||
assert_eq!(
|
||||
String::from_utf8(response.body().to_vec()).unwrap(),
|
||||
r#"{"data":{"hero":{"name":"R2-D2"}}}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_requests_work() {
|
||||
use juniper::tests::model::Database;
|
||||
use juniper::{EmptyMutation, RootNode};
|
||||
|
||||
type Schema = juniper::RootNode<'static, Database, EmptyMutation<Database>>;
|
||||
|
||||
let schema: Schema = RootNode::new(Database::new(), EmptyMutation::<Database>::new());
|
||||
|
||||
let state = warp::any().map(move || Database::new());
|
||||
let filter = warp::path("graphql2").and(make_graphql_filter(schema, state.boxed()));
|
||||
|
||||
let response = request()
|
||||
.method("POST")
|
||||
.path("/graphql2")
|
||||
.header("accept", "application/json")
|
||||
.header("content-type", "application/json")
|
||||
.body(
|
||||
r##"[
|
||||
{ "variables": null, "query": "{ hero(episode: NEW_HOPE) { name } }" },
|
||||
{ "variables": null, "query": "{ hero(episode: EMPIRE) { id name } }" }
|
||||
]"##,
|
||||
).reply(&filter);
|
||||
|
||||
assert_eq!(response.status(), http::StatusCode::OK);
|
||||
assert_eq!(
|
||||
String::from_utf8(response.body().to_vec()).unwrap(),
|
||||
r#"[{"data":{"hero":{"name":"R2-D2"}}},{"data":{"hero":{"id":"1000","name":"Luke Skywalker"}}}]"#
|
||||
);
|
||||
assert_eq!(
|
||||
response.headers().get("content-type").unwrap(),
|
||||
"application/json",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_request_deserialization_can_fail() {
|
||||
let json = r#"blah"#;
|
||||
let result: Result<GraphQLBatchRequest, _> = serde_json::from_str(json);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_http_harness {
|
||||
use super::*;
|
||||
use juniper::http::tests::{run_http_test_suite, HTTPIntegration, TestResponse};
|
||||
use juniper::tests::model::Database;
|
||||
use juniper::EmptyMutation;
|
||||
use juniper::RootNode;
|
||||
use warp;
|
||||
use warp::Filter;
|
||||
|
||||
type Schema = juniper::RootNode<'static, Database, EmptyMutation<Database>>;
|
||||
|
||||
fn warp_server() -> warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)> {
|
||||
let schema: Schema = RootNode::new(Database::new(), EmptyMutation::<Database>::new());
|
||||
|
||||
let state = warp::any().map(move || Database::new());
|
||||
let filter = warp::filters::path::index().and(make_graphql_filter(schema, state.boxed()));
|
||||
|
||||
filter.boxed()
|
||||
}
|
||||
|
||||
struct TestWarpIntegration {
|
||||
filter: warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)>,
|
||||
}
|
||||
|
||||
// This can't be implemented with the From trait since TestResponse is not defined in this crate.
|
||||
fn test_response_from_http_response(response: warp::http::Response<Vec<u8>>) -> TestResponse {
|
||||
TestResponse {
|
||||
status_code: response.status().as_u16() as i32,
|
||||
body: Some(String::from_utf8(response.body().to_owned()).unwrap()),
|
||||
content_type: response
|
||||
.headers()
|
||||
.get("content-type")
|
||||
.expect("missing content-type header in warp response")
|
||||
.to_str()
|
||||
.expect("invalid content-type string")
|
||||
.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
impl HTTPIntegration for TestWarpIntegration {
|
||||
fn get(&self, url: &str) -> TestResponse {
|
||||
use percent_encoding::{percent_encode, DEFAULT_ENCODE_SET};
|
||||
let url: String = percent_encode(url.replace("/?", "").as_bytes(), DEFAULT_ENCODE_SET)
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
|
||||
let response = warp::test::request()
|
||||
.method("GET")
|
||||
.path(&format!("/?{}", url))
|
||||
.filter(&self.filter)
|
||||
.unwrap_or_else(|rejection| {
|
||||
warp::http::Response::builder()
|
||||
.status(rejection.status())
|
||||
.header("content-type", "application/json")
|
||||
.body(Vec::new())
|
||||
.unwrap()
|
||||
});
|
||||
test_response_from_http_response(response)
|
||||
}
|
||||
|
||||
fn post(&self, url: &str, body: &str) -> TestResponse {
|
||||
let response = warp::test::request()
|
||||
.method("POST")
|
||||
.header("content-type", "application/json")
|
||||
.path(url)
|
||||
.body(body)
|
||||
.filter(&self.filter)
|
||||
.unwrap_or_else(|rejection| {
|
||||
warp::http::Response::builder()
|
||||
.status(rejection.status())
|
||||
.header("content-type", "application/json")
|
||||
.body(Vec::new())
|
||||
.unwrap()
|
||||
});
|
||||
test_response_from_http_response(response)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_warp_integration() {
|
||||
let integration = TestWarpIntegration {
|
||||
filter: warp_server(),
|
||||
};
|
||||
|
||||
run_http_test_suite(&integration);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue