Repository: graphql-rust/juniper (language: rust)

Issue:
Add hyper example/integration
I would be great to have a juniper crate (or example) that integrates with "vanilla" hyper


Discussion / hints:
We can definitely write up an example, probably using https://github.com/alexcrichton/futures-rs for executing the Juniper part.

I think vanilla Hyper might be  too edge case or to use-case specific to have an integration crate though.
PS: what I actually meant to link to was https://github.com/alexcrichton/futures-rs/tree/master/futures-cpupool
My thinking was we have Iron and Rocket integration, but generally don't leverage that many features of them as all the GraphQL queries are on one route and merely passed through to `juniper` via the framework. Most the stuff Juniper needs (status codes, http method, getting a query string, etc) is in `http`/`hyper`. For most standalone GraphQL services merely using Hyper+juniper would likely be sufficient without the added complexity or dependencies of a web framework.
@theduke @mhallin Does `juniper` work w/ `hyper@0.11.x`? All my attempts to run it are failed due to lifetime issues and I'm not sure if it's me or it just doesn't supposed to work.
nvm, it worked

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index d6e649375..08120b82d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,6 +3,7 @@ members = [
   "juniper",
   "juniper_codegen",
   "juniper_tests",
+  "juniper_hyper",
   "juniper_iron",
   "juniper_rocket",
 ]
diff --git a/README.md b/README.md
index bb90029a7..ab14ac925 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ GraphQL schemas as convenient as possible as Rust will allow.
 
 Juniper does not include a web server - instead it provides building blocks to
 make integration with existing servers straightforward. It optionally provides a
-pre-built integration for the [Iron][iron] and [Rocket] frameworks, including
+pre-built integration for the [Hyper][hyper], [Iron][iron], and [Rocket] frameworks, including
 embedded [Graphiql][graphiql] for easy debugging.
 
 * [Cargo crate](https://crates.io/crates/juniper)
@@ -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 [rocket][rocket_examples] and [iron][iron_examples] examples folders.
+see the [hyper][hyper_examples], [rocket][rocket_examples], and [iron][iron_examples] examples folders.
 
 
 ## Features
@@ -90,8 +90,10 @@ Juniper has not reached 1.0 yet, thus some API instability should be expected.
 [graphql_spec]: http://facebook.github.io/graphql
 [test_schema_rs]: https://github.com/graphql-rust/juniper/blob/master/juniper/src/tests/schema.rs
 [tokio]: https://github.com/tokio-rs/tokio
+[hyper_examples]: https://github.com/graphql-rust/juniper/tree/master/hyper_rocket/examples
 [rocket_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_rocket/examples
 [iron_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_iron/examples
+[Hyper]: https://hyper.rs
 [Rocket]: https://rocket.rs
 [book]: https://graphql-rust.github.io
 [book_quickstart]: https://graphql-rust.github.io/quickstart.html
diff --git a/juniper/src/http/mod.rs b/juniper/src/http/mod.rs
index 8319b83b9..591d62640 100644
--- a/juniper/src/http/mod.rs
+++ b/juniper/src/http/mod.rs
@@ -180,7 +180,8 @@ pub mod tests {
     }
 
     fn test_simple_get<T: HTTPIntegration>(integration: &T) {
-        let response = integration.get("/?query={hero{name}}");
+        // {hero{name}}
+        let response = integration.get("/?query=%7Bhero%7Bname%7D%7D");
 
         assert_eq!(response.status_code, 200);
         assert_eq!(response.content_type.as_str(), "application/json");
@@ -193,8 +194,9 @@ pub mod tests {
     }
 
     fn test_encoded_get<T: HTTPIntegration>(integration: &T) {
+        // query { human(id: "1000") { id, name, appearsIn, homePlanet } }
         let response = integration.get(
-            "/?query=query%20{%20%20%20human(id:%20\"1000\")%20{%20%20%20%20%20id,%20%20%20%20%20name,%20%20%20%20%20appearsIn,%20%20%20%20%20homePlanet%20%20%20}%20}");
+            "/?query=query%20%7B%20human(id%3A%20%221000%22)%20%7B%20id%2C%20name%2C%20appearsIn%2C%20homePlanet%20%7D%20%7D");
 
         assert_eq!(response.status_code, 200);
         assert_eq!(response.content_type.as_str(), "application/json");
@@ -221,8 +223,10 @@ pub mod tests {
     }
 
     fn test_get_with_variables<T: HTTPIntegration>(integration: &T) {
+        // query($id: String!) { human(id: $id) { id, name, appearsIn, homePlanet } }
+        // with variables = { "id": "1000" }
         let response = integration.get(
-            "/?query=query($id:%20String!)%20{%20%20%20human(id:%20$id)%20{%20%20%20%20%20id,%20%20%20%20%20name,%20%20%20%20%20appearsIn,%20%20%20%20%20homePlanet%20%20%20}%20}&variables={%20%20%20\"id\":%20%20\"1000\"%20}");
+            "/?query=query(%24id%3A%20String!)%20%7B%20human(id%3A%20%24id)%20%7B%20id%2C%20name%2C%20appearsIn%2C%20homePlanet%20%7D%20%7D&variables=%7B%20%22id%22%3A%20%221000%22%20%7D");
 
         assert_eq!(response.status_code, 200);
         assert_eq!(response.content_type, "application/json");
diff --git a/juniper_hyper/.gitignore b/juniper_hyper/.gitignore
new file mode 100644
index 000000000..a9d37c560
--- /dev/null
+++ b/juniper_hyper/.gitignore
@@ -0,0 +1,2 @@
+target
+Cargo.lock
diff --git a/juniper_hyper/Cargo.toml b/juniper_hyper/Cargo.toml
new file mode 100644
index 000000000..10285d3da
--- /dev/null
+++ b/juniper_hyper/Cargo.toml
@@ -0,0 +1,29 @@
+[package]
+name = "juniper_hyper"
+version = "0.1.0"
+authors = ["Damir Vandic <info@dvic.io>"]
+description = "Juniper GraphQL integration with Hyper"
+license = "BSD-2-Clause"
+documentation = "https://docs.rs/juniper_hyper"
+repository = "https://github.com/graphql-rust/juniper"
+
+[dependencies]
+serde = "1.0"
+serde_json = "1.0"
+serde_derive = "1.0"
+url = "1.7"
+juniper = { version = "0.9" , default-features = false, path = "../juniper"}
+
+futures = "0.1"
+futures-cpupool = "0.1"
+hyper = "0.12"
+
+[dev-dependencies]
+pretty_env_logger = "0.2"
+tokio = "0.1.8"
+reqwest = { git = "https://github.com/seanmonstar/reqwest" }
+
+[dev-dependencies.juniper]
+version = "0.9"
+features = ["expose-test-schema", "serde_json"]
+path = "../juniper"
diff --git a/juniper_hyper/LICENSE b/juniper_hyper/LICENSE
new file mode 100644
index 000000000..0ccd1e17c
--- /dev/null
+++ b/juniper_hyper/LICENSE
@@ -0,0 +1,25 @@
+BSD 2-Clause License
+
+Copyright (c) 2016, Magnus Hallin
+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.
\ No newline at end of file
diff --git a/juniper_hyper/Makefile.toml b/juniper_hyper/Makefile.toml
new file mode 100644
index 000000000..fd86490c8
--- /dev/null
+++ b/juniper_hyper/Makefile.toml
@@ -0,0 +1,18 @@
+
+[tasks.build-verbose]
+condition = { channels = ["nightly"] }
+
+[tasks.build-verbose.windows]
+condition = { channels = ["nightly"], env = { "TARGET" = "x86_64-pc-windows-msvc" } }
+
+[tasks.test-verbose]
+condition = { channels = ["nightly"] }
+
+[tasks.test-verbose.windows]
+condition = { channels = ["nightly"], env = { "TARGET" = "x86_64-pc-windows-msvc" } }
+
+[tasks.ci-coverage-flow]
+condition = { channels = ["nightly"] }
+
+[tasks.ci-coverage-flow.windows]
+disabled = true
diff --git a/juniper_hyper/README.md b/juniper_hyper/README.md
new file mode 100644
index 000000000..1c874f27c
--- /dev/null
+++ b/juniper_hyper/README.md
@@ -0,0 +1,35 @@
+# juniper_hyper
+
+This repository contains the [Hyper][Hyper] 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/hyper_server.rs][example] for example code of a working Hyper 
+server with GraphQL handlers.
+
+## Links
+
+* [Juniper][Juniper]
+* [API documentation][documentation]
+* [Hyper][Hyper]
+
+## License
+
+This project is under the BSD-2 license.
+
+Check the LICENSE file for details.
+
+[Hyper]: https://hyper.rs
+[Juniper]: https://github.com/graphql-rust/juniper
+[GraphQL]: http://graphql.org
+[documentation]: https://docs.rs/juniper_hyper
+[example]: https://github.com/graphql-rust/juniper/blob/hyper-integration/juniper_hyper/examples/hyper_server.rs
+
+
diff --git a/juniper_hyper/examples/hyper_server.rs b/juniper_hyper/examples/hyper_server.rs
new file mode 100644
index 000000000..ab1462715
--- /dev/null
+++ b/juniper_hyper/examples/hyper_server.rs
@@ -0,0 +1,54 @@
+extern crate futures;
+extern crate futures_cpupool;
+extern crate hyper;
+extern crate juniper;
+extern crate juniper_hyper;
+extern crate pretty_env_logger;
+
+use futures::future;
+use futures_cpupool::Builder as CpuPoolBuilder;
+use hyper::rt::{self, Future};
+use hyper::service::service_fn;
+use hyper::Method;
+use hyper::{Body, Response, Server, StatusCode};
+use juniper::tests::model::Database;
+use juniper::EmptyMutation;
+use juniper::RootNode;
+use std::sync::Arc;
+
+fn main() {
+    pretty_env_logger::init();
+
+    let addr = ([127, 0, 0, 1], 3000).into();
+
+    let pool = CpuPoolBuilder::new().create();
+    let db = Arc::new(Database::new());
+    let root_node = Arc::new(RootNode::new(db.clone(), EmptyMutation::<Database>::new()));
+
+    let new_service = move || {
+        let pool = pool.clone();
+        let root_node = root_node.clone();
+        let ctx = db.clone();
+        service_fn(move |req| {
+            let pool = pool.clone();
+            let root_node = root_node.clone();
+            let ctx = ctx.clone();
+            match (req.method(), req.uri().path()) {
+                (&Method::GET, "/") => juniper_hyper::graphiql("/graphql"),
+                (&Method::GET, "/graphql") => juniper_hyper::graphql(pool, root_node, ctx, req),
+                (&Method::POST, "/graphql") => juniper_hyper::graphql(pool, root_node, ctx, req),
+                _ => {
+                    let mut response = Response::new(Body::empty());
+                    *response.status_mut() = StatusCode::NOT_FOUND;
+                    Box::new(future::ok(response))
+                }
+            }
+        })
+    };
+    let server = Server::bind(&addr)
+        .serve(new_service)
+        .map_err(|e| eprintln!("server error: {}", e));
+    println!("Listening on http://{}", addr);
+
+    rt::run(server);
+}
diff --git a/juniper_hyper/src/lib.rs b/juniper_hyper/src/lib.rs
new file mode 100644
index 000000000..a52242921
--- /dev/null
+++ b/juniper_hyper/src/lib.rs
@@ -0,0 +1,382 @@
+#![feature(extern_prelude)]
+
+extern crate futures;
+extern crate futures_cpupool;
+extern crate hyper;
+extern crate juniper;
+#[macro_use]
+extern crate serde_derive;
+#[cfg(test)]
+extern crate reqwest;
+extern crate serde_json;
+#[cfg(test)]
+extern crate tokio;
+extern crate url;
+
+use futures::{future, Future};
+use futures_cpupool::CpuPool;
+use hyper::header::HeaderValue;
+use hyper::rt::Stream;
+use hyper::{header, Body, Method, Request, Response, StatusCode};
+use juniper::http::{
+    GraphQLRequest as JuniperGraphQLRequest, GraphQLResponse as JuniperGraphQLResponse,
+};
+use juniper::{GraphQLType, InputValue, RootNode};
+use serde_json::error::Error as SerdeError;
+use std::error::Error;
+use std::fmt;
+use std::string::FromUtf8Error;
+use std::sync::Arc;
+use url::form_urlencoded;
+
+pub fn graphql<CtxT, QueryT, MutationT>(
+    pool: CpuPool,
+    root_node: Arc<RootNode<'static, QueryT, MutationT>>,
+    context: Arc<CtxT>,
+    request: Request<Body>,
+) -> Box<Future<Item = Response<Body>, Error = hyper::Error> + Send>
+where
+    CtxT: Send + Sync + 'static,
+    QueryT: GraphQLType<Context = CtxT> + Send + Sync + 'static,
+    MutationT: GraphQLType<Context = CtxT> + Send + Sync + 'static,
+    QueryT::TypeInfo: Send + Sync,
+    MutationT::TypeInfo: Send + Sync,
+{
+    match request.method() {
+        &Method::GET => Box::new(
+            future::done(
+                request
+                    .uri()
+                    .query()
+                    .map(|q| gql_request_from_get(q).map(GraphQLRequest::Single))
+                    .unwrap_or(Err(GraphQLRequestError::Invalid(
+                        "'query' parameter is missing".to_string(),
+                    ))),
+            ).and_then(move |gql_req| execute_request(pool, root_node, context, gql_req))
+            .or_else(|err| future::ok(render_error(err))),
+        ),
+        &Method::POST => Box::new(
+            request
+                .into_body()
+                .concat2()
+                .or_else(|err| future::done(Err(GraphQLRequestError::BodyHyper(err))))
+                .and_then(move |chunk| {
+                    future::done({
+                        String::from_utf8(chunk.iter().cloned().collect::<Vec<u8>>())
+                            .map_err(GraphQLRequestError::BodyUtf8)
+                            .and_then(|input| {
+                                serde_json::from_str::<GraphQLRequest>(&input)
+                                    .map_err(GraphQLRequestError::BodyJSONError)
+                            })
+                    })
+                }).and_then(move |gql_req| execute_request(pool, root_node, context, gql_req))
+                .or_else(|err| future::ok(render_error(err))),
+        ),
+        _ => return Box::new(future::ok(new_response(StatusCode::METHOD_NOT_ALLOWED))),
+    }
+}
+
+pub fn graphiql(
+    graphql_endpoint: &str,
+) -> Box<Future<Item = Response<Body>, Error = hyper::Error> + Send> {
+    let mut resp = new_html_response(StatusCode::OK);
+    *resp.body_mut() = Body::from(juniper::graphiql::graphiql_source(graphql_endpoint));
+    Box::new(future::ok(resp))
+}
+
+fn render_error(err: GraphQLRequestError) -> Response<Body> {
+    let message = format!("{}", err);
+    let mut resp = new_response(StatusCode::BAD_REQUEST);
+    *resp.body_mut() = Body::from(message);
+    resp
+}
+
+fn execute_request<CtxT, QueryT, MutationT, Err>(
+    pool: CpuPool,
+    root_node: Arc<RootNode<'static, QueryT, MutationT>>,
+    context: Arc<CtxT>,
+    request: GraphQLRequest,
+) -> impl Future<Item = Response<Body>, Error = Err>
+where
+    CtxT: Send + Sync + 'static,
+    QueryT: GraphQLType<Context = CtxT> + Send + Sync + 'static,
+    MutationT: GraphQLType<Context = CtxT> + Send + Sync + 'static,
+    QueryT::TypeInfo: Send + Sync,
+    MutationT::TypeInfo: Send + Sync,
+    Err: Send + Sync + 'static,
+{
+    pool.spawn_fn(move || {
+        future::lazy(move || {
+            let res = request.execute(&root_node, &context);
+            let code = if res.is_ok() {
+                StatusCode::OK
+            } else {
+                StatusCode::BAD_REQUEST
+            };
+            let mut resp = new_response(code);
+            resp.headers_mut().insert(
+                header::CONTENT_TYPE,
+                HeaderValue::from_static("application/json"),
+            );
+            *resp.body_mut() = Body::from(serde_json::to_string_pretty(&res).unwrap());
+            future::ok(resp)
+        })
+    })
+}
+
+fn gql_request_from_get(input: &str) -> Result<JuniperGraphQLRequest, GraphQLRequestError> {
+    let mut query = None;
+    let operation_name = None;
+    let mut variables = None;
+    for (key, value) in form_urlencoded::parse(input.as_bytes()).into_owned() {
+        match key.as_ref() {
+            "query" => {
+                if query.is_some() {
+                    return Err(invalid_err("query"));
+                }
+                query = Some(value)
+            }
+            "operationName" => {
+                if operation_name.is_some() {
+                    return Err(invalid_err("operationName"));
+                }
+            }
+            "variables" => {
+                if variables.is_some() {
+                    return Err(invalid_err("variables"));
+                }
+                match serde_json::from_str::<InputValue>(&value)
+                    .map_err(GraphQLRequestError::Variables)
+                {
+                    Ok(parsed_variables) => variables = Some(parsed_variables),
+                    Err(e) => return Err(e),
+                }
+            }
+            _ => continue,
+        }
+    }
+    match query {
+        Some(query) => Ok(JuniperGraphQLRequest::new(query, operation_name, variables)),
+        None => Err(GraphQLRequestError::Invalid(
+            "'query' parameter is missing".to_string(),
+        )),
+    }
+}
+
+fn invalid_err(parameter_name: &str) -> GraphQLRequestError {
+    GraphQLRequestError::Invalid(format!(
+        "'{}' parameter is specified multiple times",
+        parameter_name
+    ))
+}
+
+fn new_response(code: StatusCode) -> Response<Body> {
+    let mut r = Response::new(Body::empty());
+    *r.status_mut() = code;
+    r
+}
+
+fn new_html_response(code: StatusCode) -> Response<Body> {
+    let mut resp = new_response(code);
+    resp.headers_mut().insert(
+        header::CONTENT_TYPE,
+        HeaderValue::from_static("text/html; charset=utf-8"),
+    );
+    resp
+}
+
+#[derive(Deserialize)]
+#[serde(untagged)]
+enum GraphQLRequest {
+    Single(JuniperGraphQLRequest),
+    Batch(Vec<JuniperGraphQLRequest>),
+}
+
+impl GraphQLRequest {
+    pub fn execute<'a, CtxT, QueryT, MutationT>(
+        &'a self,
+        root_node: &RootNode<QueryT, MutationT>,
+        context: &CtxT,
+    ) -> GraphQLResponse<'a>
+    where
+        QueryT: GraphQLType<Context = CtxT>,
+        MutationT: GraphQLType<Context = CtxT>,
+    {
+        match self {
+            &GraphQLRequest::Single(ref request) => {
+                GraphQLResponse::Single(request.execute(root_node, context))
+            }
+            &GraphQLRequest::Batch(ref requests) => GraphQLResponse::Batch(
+                requests
+                    .iter()
+                    .map(|request| request.execute(root_node, context))
+                    .collect(),
+            ),
+        }
+    }
+}
+
+#[derive(Serialize)]
+#[serde(untagged)]
+enum GraphQLResponse<'a> {
+    Single(JuniperGraphQLResponse<'a>),
+    Batch(Vec<JuniperGraphQLResponse<'a>>),
+}
+
+impl<'a> GraphQLResponse<'a> {
+    fn is_ok(&self) -> bool {
+        match self {
+            &GraphQLResponse::Single(ref response) => response.is_ok(),
+            &GraphQLResponse::Batch(ref responses) => responses
+                .iter()
+                .fold(true, |ok, response| ok && response.is_ok()),
+        }
+    }
+}
+
+#[derive(Debug)]
+enum GraphQLRequestError {
+    BodyHyper(hyper::Error),
+    BodyUtf8(FromUtf8Error),
+    BodyJSONError(SerdeError),
+    Variables(SerdeError),
+    Invalid(String),
+}
+
+impl fmt::Display for GraphQLRequestError {
+    fn fmt(&self, mut f: &mut fmt::Formatter) -> fmt::Result {
+        match *self {
+            GraphQLRequestError::BodyHyper(ref err) => fmt::Display::fmt(err, &mut f),
+            GraphQLRequestError::BodyUtf8(ref err) => fmt::Display::fmt(err, &mut f),
+            GraphQLRequestError::BodyJSONError(ref err) => fmt::Display::fmt(err, &mut f),
+            GraphQLRequestError::Variables(ref err) => fmt::Display::fmt(err, &mut f),
+            GraphQLRequestError::Invalid(ref err) => fmt::Display::fmt(err, &mut f),
+        }
+    }
+}
+
+impl Error for GraphQLRequestError {
+    fn description(&self) -> &str {
+        match *self {
+            GraphQLRequestError::BodyHyper(ref err) => err.description(),
+            GraphQLRequestError::BodyUtf8(ref err) => err.description(),
+            GraphQLRequestError::BodyJSONError(ref err) => err.description(),
+            GraphQLRequestError::Variables(ref err) => err.description(),
+            GraphQLRequestError::Invalid(ref err) => err,
+        }
+    }
+
+    fn cause(&self) -> Option<&Error> {
+        match *self {
+            GraphQLRequestError::BodyHyper(ref err) => Some(err),
+            GraphQLRequestError::BodyUtf8(ref err) => Some(err),
+            GraphQLRequestError::BodyJSONError(ref err) => Some(err),
+            GraphQLRequestError::Variables(ref err) => Some(err),
+            GraphQLRequestError::Invalid(_) => None,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use futures::{future, Future};
+    use futures_cpupool::Builder;
+    use hyper::service::service_fn;
+    use hyper::Method;
+    use hyper::{header, Body, Response, Server, StatusCode};
+    use juniper::http::tests as http_tests;
+    use juniper::tests::model::Database;
+    use juniper::EmptyMutation;
+    use juniper::RootNode;
+    use reqwest;
+    use reqwest::Response as ReqwestResponse;
+    use std::sync::Arc;
+    use std::thread;
+    use std::time;
+    use tokio::runtime::Runtime;
+
+    struct TestHyperIntegration;
+
+    impl http_tests::HTTPIntegration for TestHyperIntegration {
+        fn get(&self, url: &str) -> http_tests::TestResponse {
+            let url = format!("http://127.0.0.1:3001/graphql{}", url);
+            make_test_response(reqwest::get(&url).expect(&format!("failed GET {}", url)))
+        }
+
+        fn post(&self, url: &str, body: &str) -> http_tests::TestResponse {
+            let url = format!("http://127.0.0.1:3001/graphql{}", url);
+            let client = reqwest::Client::new();
+            let res = client
+                .post(&url)
+                .body(body.to_string())
+                .send()
+                .expect(&format!("failed POST {}", url));
+            make_test_response(res)
+        }
+    }
+
+    fn make_test_response(mut response: ReqwestResponse) -> http_tests::TestResponse {
+        let status_code = response.status().as_u16() as i32;
+        let content_type = String::from_utf8(
+            response
+                .headers()
+                .get(header::CONTENT_TYPE)
+                .map(|h| h.clone().as_ref().to_vec())
+                .unwrap_or(vec![]),
+        ).expect("Content-type header invalid UTF-8");
+
+        let body = response.text().unwrap();
+
+        http_tests::TestResponse {
+            status_code,
+            body: Some(body),
+            content_type,
+        }
+    }
+
+    #[test]
+    fn test_hyper_integration() {
+        let addr = ([127, 0, 0, 1], 3001).into();
+
+        let pool = Builder::new().create();
+        let db = Arc::new(Database::new());
+        let root_node = Arc::new(RootNode::new(db.clone(), EmptyMutation::<Database>::new()));
+
+        let new_service = move || {
+            let pool = pool.clone();
+            let root_node = root_node.clone();
+            let ctx = db.clone();
+            service_fn(move |req| {
+                let pool = pool.clone();
+                let root_node = root_node.clone();
+                let ctx = ctx.clone();
+                let matches = {
+                    let path = req.uri().path();
+                    match req.method() {
+                        &Method::POST | &Method::GET => path == "/graphql" || path == "/graphql/",
+                        _ => false,
+                    }
+                };
+                if matches {
+                    super::graphql(pool, root_node, ctx, req)
+                } else {
+                    let mut response = Response::new(Body::empty());
+                    *response.status_mut() = StatusCode::NOT_FOUND;
+                    Box::new(future::ok(response))
+                }
+            })
+        };
+        let server = Server::bind(&addr)
+            .serve(new_service)
+            .map_err(|e| eprintln!("server error: {}", e));
+
+        let mut runtime = Runtime::new().unwrap();
+        runtime.spawn(server);
+        thread::sleep(time::Duration::from_millis(10)); // wait 10ms for server to bind
+
+        let integration = TestHyperIntegration;
+        http_tests::run_http_test_suite(&integration);
+
+        runtime.shutdown_now().wait().unwrap();
+    }
+}


Test patch: