Hyper integration crate (#230)

This adds the new integration crate `juniper_hyper`.
This commit is contained in:
Damir Vandic 2018-09-02 05:02:01 +02:00 committed by Christian Legnitto
parent facb0d2f5d
commit 44002f0884
10 changed files with 557 additions and 5 deletions

View file

@ -3,6 +3,7 @@ members = [
"juniper", "juniper",
"juniper_codegen", "juniper_codegen",
"juniper_tests", "juniper_tests",
"juniper_hyper",
"juniper_iron", "juniper_iron",
"juniper_rocket", "juniper_rocket",
] ]

View file

@ -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 Juniper does not include a web server - instead it provides building blocks to
make integration with existing servers straightforward. It optionally provides a 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. embedded [Graphiql][graphiql] for easy debugging.
* [Cargo crate](https://crates.io/crates/juniper) * [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 You can also check out [src/tests/schema.rs][test_schema_rs] to see a complex
schema including polymorphism with traits and interfaces. schema including polymorphism with traits and interfaces.
For an example of web framework integration, 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 ## 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 [graphql_spec]: http://facebook.github.io/graphql
[test_schema_rs]: https://github.com/graphql-rust/juniper/blob/master/juniper/src/tests/schema.rs [test_schema_rs]: https://github.com/graphql-rust/juniper/blob/master/juniper/src/tests/schema.rs
[tokio]: https://github.com/tokio-rs/tokio [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 [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 [iron_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_iron/examples
[Hyper]: https://hyper.rs
[Rocket]: https://rocket.rs [Rocket]: https://rocket.rs
[book]: https://graphql-rust.github.io [book]: https://graphql-rust.github.io
[book_quickstart]: https://graphql-rust.github.io/quickstart.html [book_quickstart]: https://graphql-rust.github.io/quickstart.html

View file

@ -180,7 +180,8 @@ pub mod tests {
} }
fn test_simple_get<T: HTTPIntegration>(integration: &T) { 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.status_code, 200);
assert_eq!(response.content_type.as_str(), "application/json"); assert_eq!(response.content_type.as_str(), "application/json");
@ -193,8 +194,9 @@ pub mod tests {
} }
fn test_encoded_get<T: HTTPIntegration>(integration: &T) { fn test_encoded_get<T: HTTPIntegration>(integration: &T) {
// query { human(id: "1000") { id, name, appearsIn, homePlanet } }
let response = integration.get( 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.status_code, 200);
assert_eq!(response.content_type.as_str(), "application/json"); 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) { 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( 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.status_code, 200);
assert_eq!(response.content_type, "application/json"); assert_eq!(response.content_type, "application/json");

2
juniper_hyper/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
target
Cargo.lock

29
juniper_hyper/Cargo.toml Normal file
View 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
View 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.

View 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
View 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

View 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
View 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();
}
}