Hyper integration crate (#230)
This adds the new integration crate `juniper_hyper`.
This commit is contained in:
parent
facb0d2f5d
commit
44002f0884
10 changed files with 557 additions and 5 deletions
|
@ -3,6 +3,7 @@ members = [
|
|||
"juniper",
|
||||
"juniper_codegen",
|
||||
"juniper_tests",
|
||||
"juniper_hyper",
|
||||
"juniper_iron",
|
||||
"juniper_rocket",
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
2
juniper_hyper/.gitignore
vendored
Normal file
2
juniper_hyper/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
target
|
||||
Cargo.lock
|
29
juniper_hyper/Cargo.toml
Normal file
29
juniper_hyper/Cargo.toml
Normal file
|
@ -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"
|
25
juniper_hyper/LICENSE
Normal file
25
juniper_hyper/LICENSE
Normal file
|
@ -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.
|
18
juniper_hyper/Makefile.toml
Normal file
18
juniper_hyper/Makefile.toml
Normal file
|
@ -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
|
35
juniper_hyper/README.md
Normal file
35
juniper_hyper/README.md
Normal file
|
@ -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
|
||||
|
||||
|
54
juniper_hyper/examples/hyper_server.rs
Normal file
54
juniper_hyper/examples/hyper_server.rs
Normal file
|
@ -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);
|
||||
}
|
382
juniper_hyper/src/lib.rs
Normal file
382
juniper_hyper/src/lib.rs
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue