From b89712a8878f46b26fdbbf815d8a650a01a29d25 Mon Sep 17 00:00:00 2001 From: theduke Date: Sat, 2 Dec 2017 15:59:23 +0100 Subject: [PATCH] Add juniper_rocket back to main repo This is done for a lower maintainance burden and combined testing. --- juniper_rocket/.gitignore | 2 + juniper_rocket/Cargo.toml | 23 +++ juniper_rocket/LICENSE | 25 +++ juniper_rocket/README.md | 31 +++ juniper_rocket/examples/rocket_server.rs | 51 +++++ juniper_rocket/src/lib.rs | 232 +++++++++++++++++++++++ 6 files changed, 364 insertions(+) create mode 100644 juniper_rocket/.gitignore create mode 100644 juniper_rocket/Cargo.toml create mode 100644 juniper_rocket/LICENSE create mode 100644 juniper_rocket/README.md create mode 100644 juniper_rocket/examples/rocket_server.rs create mode 100644 juniper_rocket/src/lib.rs diff --git a/juniper_rocket/.gitignore b/juniper_rocket/.gitignore new file mode 100644 index 00000000..a9d37c56 --- /dev/null +++ b/juniper_rocket/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/juniper_rocket/Cargo.toml b/juniper_rocket/Cargo.toml new file mode 100644 index 00000000..6f1c6fac --- /dev/null +++ b/juniper_rocket/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "juniper_rocket" +version = "0.1.0" +authors = ["Magnus Hallin "] +description = "Juniper GraphQL integration with Rocket" +license = "BSD-2-Clause" +documentation = "https://docs.rs/juniper_rocket" +repository = "https://github.com/graphql-rust/juniper_rocket" + +[dependencies] +serde = { version = "1.0.8" } +serde_derive = {version="1.0.8" } +serde_json = { version = "1.0.2" } +rocket = { version = "0.3.0" } +rocket_codegen = { version = "0.3.0" } +juniper = { version = "0.8.1" , git = "https://github.com/graphql-rust/juniper" } + +[badges] +travis-ci = { repository = "mhallin/juniper" } +appveyor = { repository = "mhallin/juniper" } + +[dev-dependencies] +juniper = { version = "0.8.1", path = "../juniper", features=["expose-test-schema", "serde_json"], git = "https://github.com/graphql-rust/juniper" } diff --git a/juniper_rocket/LICENSE b/juniper_rocket/LICENSE new file mode 100644 index 00000000..0ccd1e17 --- /dev/null +++ b/juniper_rocket/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_rocket/README.md b/juniper_rocket/README.md new file mode 100644 index 00000000..b099f940 --- /dev/null +++ b/juniper_rocket/README.md @@ -0,0 +1,31 @@ +# juniper_rocket + +[![Build Status](https://travis-ci.org/graphql-rust/juniper_rocket.svg?branch=master)](https://travis-ci.org/graphql-rust/juniper_rocket) +[![Build status](https://ci.appveyor.com/api/projects/status/9j9bvj7q05jcxw2v?svg=true)](https://ci.appveyor.com/project/theduke/juniper-rocket) +[![Crates.io](https://img.shields.io/crates/v/juniper_rocket.svg?maxAge=2592000)](https://crates.io/crates/juniper_rocket) +[![Gitter chat](https://badges.gitter.im/juniper-graphql/gitter.png)](https://gitter.im/juniper-graphql) + +This repository contains the [Rocket][Rocket] web server integration for [Juniper][Juniper], a [GraphQL][GraphQL] +implementation for Rust. + +## Documentation + +Once the crate is published, the documentation will be on [docs.rs][documentation]. + +For now, please consult the example below. + +## Examples + +Check [examples/rocket_server.rs][example] for example code of a working Rocket server with GraphQL handlers. + +## License + +This project is under the BSD-2 license. + +Check the LICENSE file for details. + +[Rocket]: https://rocket.rs +[Juniper]: https://github.com/graphql-rust/juniper +[GraphQL]: http://graphql.org +[documentation]: https://docs.rs/juniper_rocket +[example]: https://github.com/graphql-rust/juniper_rocket/blob/master/examples/rocket_server.rs diff --git a/juniper_rocket/examples/rocket_server.rs b/juniper_rocket/examples/rocket_server.rs new file mode 100644 index 00000000..82f42ea2 --- /dev/null +++ b/juniper_rocket/examples/rocket_server.rs @@ -0,0 +1,51 @@ +#![feature(plugin)] +#![plugin(rocket_codegen)] + +extern crate rocket; +extern crate juniper; +extern crate juniper_rocket; + +use rocket::response::content; +use rocket::State; + +use juniper::tests::model::Database; +use juniper::{EmptyMutation, RootNode}; + +type Schema = RootNode<'static, Database, EmptyMutation>; + +#[get("/")] +fn graphiql() -> content::Html { + juniper_rocket::graphiql_source("/graphql") +} + +#[get("/graphql?")] +fn get_graphql_handler( + context: State, + request: juniper_rocket::GraphQLRequest, + schema: State, +) -> juniper_rocket::GraphQLResponse { + request.execute(&schema, &context) +} + +#[post("/graphql", data = "")] +fn post_graphql_handler( + context: State, + request: juniper_rocket::GraphQLRequest, + schema: State, +) -> juniper_rocket::GraphQLResponse { + request.execute(&schema, &context) +} + +fn main() { + rocket::ignite() + .manage(Database::new()) + .manage(Schema::new( + Database::new(), + EmptyMutation::::new(), + )) + .mount( + "/", + routes![graphiql, get_graphql_handler, post_graphql_handler], + ) + .launch(); +} diff --git a/juniper_rocket/src/lib.rs b/juniper_rocket/src/lib.rs new file mode 100644 index 00000000..262fcd12 --- /dev/null +++ b/juniper_rocket/src/lib.rs @@ -0,0 +1,232 @@ +#![feature(plugin)] +#![plugin(rocket_codegen)] + +extern crate juniper; +extern crate serde_json; +extern crate rocket; + +use std::io::{Cursor, Read}; +use std::error::Error; + +use rocket::Request; +use rocket::request::{FormItems, FromForm}; +use rocket::data::{FromData, Outcome as FromDataOutcome}; +use rocket::response::{content, Responder, Response}; +use rocket::http::{ContentType, Status}; +use rocket::Data; +use rocket::Outcome::{Failure, Forward, Success}; + +use juniper::InputValue; +use juniper::http; + +use juniper::GraphQLType; +use juniper::RootNode; + +/// Simple wrapper around an incoming GraphQL request +/// +/// See the `http` module for more information. This type can be constructed +/// automatically from both GET and POST routes by implementing the `FromForm` +/// and `FromData` traits. +pub struct GraphQLRequest(http::GraphQLRequest); + +/// Simple wrapper around the result of executing a GraphQL query +pub struct GraphQLResponse(Status, String); + +/// Generate an HTML page containing GraphiQL +pub fn graphiql_source(graphql_endpoint_url: &str) -> content::Html { + content::Html(juniper::graphiql::graphiql_source(graphql_endpoint_url)) +} + +impl GraphQLRequest { + /// Execute an incoming GraphQL query + pub fn execute( + &self, + root_node: &RootNode, + context: &CtxT, + ) -> GraphQLResponse + where + QueryT: GraphQLType, + MutationT: GraphQLType, + { + let response = self.0.execute(root_node, context); + let status = if response.is_ok() { + Status::Ok + } else { + Status::BadRequest + }; + let json = serde_json::to_string_pretty(&response).unwrap(); + + GraphQLResponse(status, json) + } +} + +impl<'f> FromForm<'f> for GraphQLRequest { + type Error = String; + + fn from_form(form_items: &mut FormItems<'f>, strict: bool) -> Result { + let mut query = None; + let mut operation_name = None; + let mut variables = None; + + for (key, value) in form_items { + match key.as_str() { + "query" => if query.is_some() { + return Err("Query parameter must not occur more than once".to_owned()); + } else { + query = Some(value.as_str().to_string()); + }, + "operation_name" => if operation_name.is_some() { + return Err( + "Operation name parameter must not occur more than once".to_owned(), + ); + } else { + operation_name = Some(value.as_str().to_string()); + }, + "variables" => if variables.is_some() { + return Err( + "Variables parameter must not occur more than once".to_owned(), + ); + } else { + variables = Some(serde_json::from_str::(value.as_str()) + .map_err(|err| err.description().to_owned())?); + }, + _ => if strict { + return Err(format!("Prohibited extra field '{}'", key).to_owned()); + }, + } + } + + if let Some(query) = query { + Ok(GraphQLRequest( + http::GraphQLRequest::new(query, operation_name, variables), + )) + } else { + Err("Query parameter missing".to_owned()) + } + } +} + +impl FromData for GraphQLRequest { + type Error = String; + + fn from_data(request: &Request, data: Data) -> FromDataOutcome { + if !request.content_type().map_or(false, |ct| ct.is_json()) { + return Forward(data); + } + + let mut body = String::new(); + if let Err(e) = data.open().read_to_string(&mut body) { + return Failure((Status::InternalServerError, format!("{:?}", e))); + } + + match serde_json::from_str(&body) { + Ok(value) => Success(GraphQLRequest(value)), + Err(failure) => return Failure((Status::BadRequest, format!("{}", failure))), + } + } +} + +impl<'r> Responder<'r> for GraphQLResponse { + fn respond_to(self, _: &Request) -> Result, Status> { + let GraphQLResponse(status, body) = self; + + Ok( + Response::build() + .header(ContentType::new("application", "json")) + .status(status) + .sized_body(Cursor::new(body)) + .finalize(), + ) + } +} + +#[cfg(test)] +mod tests { + + use rocket; + use rocket::Rocket; + use rocket::http::{ContentType, Method}; + use rocket::State; + + use juniper::RootNode; + use juniper::tests::model::Database; + use juniper::http::tests as http_tests; + use juniper::EmptyMutation; + + type Schema = RootNode<'static, Database, EmptyMutation>; + + + #[get("/?")] + fn get_graphql_handler( + context: State, + request: super::GraphQLRequest, + schema: State, + ) -> super::GraphQLResponse { + request.execute(&schema, &context) + } + + #[post("/", data = "")] + fn post_graphql_handler( + context: State, + request: super::GraphQLRequest, + schema: State, + ) -> super::GraphQLResponse { + request.execute(&schema, &context) + } + + struct TestRocketIntegration { + rocket: Rocket, + } + + /* + + impl http_tests::HTTPIntegration for TestRocketIntegration + { + fn get(&self, url: &str) -> http_tests::TestResponse { + make_test_response(&self.rocket, MockRequest::new( + Method::Get, + url)) + } + + fn post(&self, url: &str, body: &str) -> http_tests::TestResponse { + make_test_response( + &self.rocket, + MockRequest::new( + Method::Post, + url, + ).header(ContentType::JSON).body(body)) + } + } + + #[test] + fn test_rocket_integration() { + let integration = TestRocketIntegration { + rocket: make_rocket(), + }; + + http_tests::run_http_test_suite(&integration); + } + + fn make_rocket() -> Rocket { + rocket::ignite() + .manage(Database::new()) + .manage(Schema::new(Database::new(), EmptyMutation::::new())) + .mount("/", routes![post_graphql_handler, get_graphql_handler]) + } + + fn make_test_response<'r>(rocket: &'r Rocket, mut request: MockRequest<'r>) -> http_tests::TestResponse { + let mut response = request.dispatch_with(&rocket); + let status_code = response.status().code as i32; + let content_type = response.header_values("content-type").collect::>().into_iter().next() + .expect("No content type header from handler").to_owned(); + let body = response.body().expect("No body returned from GraphQL handler").into_string(); + + http_tests::TestResponse { + status_code: status_code, + body: body, + content_type: content_type, + } + } + + */ +}