Extracted iron/rocket crates and updated README.
Those now live in separate repos.
This commit is contained in:
parent
0c7e39f14e
commit
2ca9baa441
11 changed files with 2 additions and 885 deletions
|
@ -3,6 +3,4 @@ members = [
|
|||
"juniper",
|
||||
"juniper_codegen",
|
||||
"juniper_tests",
|
||||
"juniper_iron",
|
||||
"juniper_rocket",
|
||||
]
|
||||
|
|
|
@ -36,7 +36,7 @@ feature flag:
|
|||
```toml
|
||||
[dependencies]
|
||||
juniper = { version = "0.8.1" }
|
||||
juniper_iron = { git = "https://github.com/mhallin/juniper" }
|
||||
juniper_iron = { git = "https://github.com/graphql-rust/juniper_iron" }
|
||||
|
||||
```
|
||||
|
||||
|
@ -45,7 +45,7 @@ If you want Rocket integration, you need to depend on the `juniper_rocket` crate
|
|||
```toml
|
||||
[dependencies]
|
||||
juniper = { version = "0.8.1" }
|
||||
juniper_rocket = { git = "https://github.com/mhallin/juniper" }
|
||||
juniper_rocket = { git = "https://github.com/graphql-rustl/juniper_rocket" }
|
||||
```
|
||||
|
||||
## Building schemas
|
||||
|
|
|
@ -51,7 +51,4 @@ build: false
|
|||
test_script:
|
||||
- cd juniper && cargo build --verbose && cargo test --verbose && cd ..
|
||||
- cd juniper_codegen && cargo build --verbose && cargo test --verbose && cd ..
|
||||
- cd juniper_iron && cargo build --verbose && cargo test --verbose && cd ..
|
||||
- cd juniper_tests && cargo build --verbose && cargo test --verbose && cd ..
|
||||
- IF NOT %TARGET% == %TARGET:msvc=% ( IF %CHANNEL% == "nightly" ( cd juniper_rocket && cargo test --verbose && cargo build --verbose && cd .. ) )
|
||||
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
# Build and test on stable, beta, and nightly on Windows.
|
||||
#
|
||||
# Copied general structure from https://github.com/japaric/rust-everywhere/blob/master/appveyor.yml
|
||||
|
||||
environment:
|
||||
matrix:
|
||||
# Stable channel
|
||||
- TARGET: i686-pc-windows-gnu
|
||||
CHANNEL: stable
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
CHANNEL: stable
|
||||
- TARGET: x86_64-pc-windows-gnu
|
||||
CHANNEL: stable
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
CHANNEL: stable
|
||||
# Beta channel
|
||||
- TARGET: i686-pc-windows-gnu
|
||||
CHANNEL: beta
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
CHANNEL: beta
|
||||
- TARGET: x86_64-pc-windows-gnu
|
||||
CHANNEL: beta
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
CHANNEL: beta
|
||||
# Nightly channel
|
||||
- TARGET: i686-pc-windows-gnu
|
||||
CHANNEL: nightly
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
CHANNEL: nightly
|
||||
- TARGET: x86_64-pc-windows-gnu
|
||||
CHANNEL: nightly
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
CHANNEL: nightly
|
||||
|
||||
# Install Rust and Cargo
|
||||
# (Based on from https://github.com/rust-lang/libc/blob/master/appveyor.yml)
|
||||
install:
|
||||
- curl -sSf -o rustup-init.exe https://win.rustup.rs
|
||||
- rustup-init.exe --default-host %TARGET% --default-toolchain %CHANNEL% -y
|
||||
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
|
||||
- rustc -Vv
|
||||
- cargo -V
|
||||
|
||||
|
||||
# 'cargo test' takes care of building for us, so disable Appveyor's build stage. This prevents
|
||||
# the "directory does not contain a project or solution file" error.
|
||||
# source: https://github.com/starkat99/appveyor-rust/blob/master/appveyor.yml#L113
|
||||
build: false
|
||||
|
||||
test_script:
|
||||
# Build library standalone, with Iron, and with Rocket integrations enabled, respectively
|
||||
- cargo build --verbose
|
||||
- cargo build --verbose --features iron-handlers
|
||||
- IF "%CHANNEL%"=="nightly" (cargo build --verbose --features rocket-handlers)
|
||||
|
||||
# Build example binaries; first Iron, then Rocket examples
|
||||
- cargo build --verbose --example server --features "iron-handlers expose-test-schema"
|
||||
- IF "%CHANNEL%"=="nightly" (cargo build --verbose --example rocket-server --features "rocket-handlers expose-test-schema")
|
||||
|
||||
# Run all tests for the base library and available integrations
|
||||
- set TEST_FEATURES=iron-handlers expose-test-schema
|
||||
- IF "%CHANNEL%"=="nightly" (set TEST_FEATURES=%TEST_FEATURES% rocket-handlers rocket/testing)
|
||||
|
||||
- cargo test --verbose --features "%TEST_FEATURES%"
|
|
@ -1,26 +0,0 @@
|
|||
[package]
|
||||
name = "juniper_iron"
|
||||
version = "0.1.0"
|
||||
authors = ["Magnus Hallin <mhallin@fastmail.com>"]
|
||||
description = "Iron integration for juniper"
|
||||
license = "BSD-2-Clause"
|
||||
documentation = "https://docs.rs/juniper_iron"
|
||||
repository = "https://github.com/mhallin/juniper"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "^1.0.2" }
|
||||
serde_json = { version = "^1.0.2" }
|
||||
urlencoded = { version = "^0.5.0" }
|
||||
iron = "^0.5.1"
|
||||
juniper = { version = "0.8.1", path = "../juniper" }
|
||||
|
||||
[dev-dependencies]
|
||||
iron-test = "^0.5.0"
|
||||
router = "^0.5.0"
|
||||
mount = "^0.3.0"
|
||||
logger = "^0.3.0"
|
||||
juniper = { version = "0.8.1", path = "../juniper" , features = ["expose-test-schema", "serde_json"] }
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "mhallin/juniper" }
|
||||
appveyor = { repository = "mhallin/juniper" }
|
|
@ -1,43 +0,0 @@
|
|||
extern crate iron;
|
||||
extern crate mount;
|
||||
extern crate logger;
|
||||
extern crate serde;
|
||||
extern crate juniper;
|
||||
extern crate juniper_iron;
|
||||
|
||||
use std::env;
|
||||
|
||||
use mount::Mount;
|
||||
use logger::Logger;
|
||||
use iron::prelude::*;
|
||||
use juniper::EmptyMutation;
|
||||
use juniper_iron::{GraphQLHandler, GraphiQLHandler};
|
||||
use juniper::tests::model::Database;
|
||||
|
||||
fn context_factory(_: &mut Request) -> Database {
|
||||
Database::new()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut mount = Mount::new();
|
||||
|
||||
let graphql_endpoint = GraphQLHandler::new(
|
||||
context_factory,
|
||||
Database::new(),
|
||||
EmptyMutation::<Database>::new(),
|
||||
);
|
||||
let graphiql_endpoint = GraphiQLHandler::new("/graphql");
|
||||
|
||||
mount.mount("/", graphiql_endpoint);
|
||||
mount.mount("/graphql", graphql_endpoint);
|
||||
|
||||
let (logger_before, logger_after) = Logger::new(None);
|
||||
|
||||
let mut chain = Chain::new(mount);
|
||||
chain.link_before(logger_before);
|
||||
chain.link_after(logger_after);
|
||||
|
||||
let host = env::var("LISTEN").unwrap_or("0.0.0.0:8080".to_owned());
|
||||
println!("GraphQL server started on {}", host);
|
||||
Iron::new(chain).http(host.as_str()).unwrap();
|
||||
}
|
|
@ -1,397 +0,0 @@
|
|||
/*!
|
||||
|
||||
[Juniper][1] handlers for the [Iron][2] framework.
|
||||
|
||||
## Integrating with Iron
|
||||
|
||||
|
||||
|
||||
For example, continuing from the schema created above and using Iron to expose
|
||||
the schema on an HTTP endpoint supporting both GET and POST requests:
|
||||
|
||||
```rust,no_run
|
||||
extern crate iron;
|
||||
# #[macro_use] extern crate juniper;
|
||||
# extern crate juniper_iron;
|
||||
# use std::collections::HashMap;
|
||||
|
||||
use iron::prelude::*;
|
||||
use juniper_iron::GraphQLHandler;
|
||||
use juniper::{Context, EmptyMutation};
|
||||
|
||||
# use juniper::FieldResult;
|
||||
#
|
||||
# struct User { id: String, name: String, friend_ids: Vec<String> }
|
||||
# struct QueryRoot;
|
||||
# struct Database { users: HashMap<String, User> }
|
||||
#
|
||||
# graphql_object!(User: Database |&self| {
|
||||
# field id() -> FieldResult<&String> {
|
||||
# Ok(&self.id)
|
||||
# }
|
||||
#
|
||||
# field name() -> FieldResult<&String> {
|
||||
# Ok(&self.name)
|
||||
# }
|
||||
#
|
||||
# field friends(&executor) -> FieldResult<Vec<&User>> {
|
||||
# Ok(self.friend_ids.iter()
|
||||
# .filter_map(|id| executor.context().users.get(id))
|
||||
# .collect())
|
||||
# }
|
||||
# });
|
||||
#
|
||||
# graphql_object!(QueryRoot: Database |&self| {
|
||||
# field user(&executor, id: String) -> FieldResult<Option<&User>> {
|
||||
# Ok(executor.context().users.get(&id))
|
||||
# }
|
||||
# });
|
||||
|
||||
// This function is executed for every request. Here, we would realistically
|
||||
// provide a database connection or similar. For this example, we'll be
|
||||
// creating the database from scratch.
|
||||
fn context_factory(_: &mut Request) -> Database {
|
||||
Database {
|
||||
users: vec![
|
||||
( "1000".to_owned(), User {
|
||||
id: "1000".to_owned(), name: "Robin".to_owned(),
|
||||
friend_ids: vec!["1001".to_owned()] } ),
|
||||
( "1001".to_owned(), User {
|
||||
id: "1001".to_owned(), name: "Max".to_owned(),
|
||||
friend_ids: vec!["1000".to_owned()] } ),
|
||||
].into_iter().collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for Database {}
|
||||
|
||||
fn main() {
|
||||
// GraphQLHandler takes a context factory function, the root object,
|
||||
// and the mutation object. If we don't have any mutations to expose, we
|
||||
// can use the empty tuple () to indicate absence.
|
||||
let graphql_endpoint = GraphQLHandler::new(
|
||||
context_factory, QueryRoot, EmptyMutation::<Database>::new());
|
||||
|
||||
// Start serving the schema at the root on port 8080.
|
||||
Iron::new(graphql_endpoint).http("localhost:8080").unwrap();
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
See the [iron_server.rs][5]
|
||||
example for more information on how to use these handlers.
|
||||
|
||||
See the the [`GraphQLHandler`][3] documentation for more information on what request methods are
|
||||
supported.
|
||||
There's also a built-in [GraphiQL][4] handler included.
|
||||
|
||||
[1]: https://github.com/mhallin/Juniper
|
||||
[2]: http://ironframework.io
|
||||
[3]: ./struct.GraphQLHandler.html
|
||||
[4]: https://github.com/graphql/graphiql
|
||||
[5]: https://github.com/mhallin/juniper/blob/master/juniper_iron/examples/iron_server.rs
|
||||
|
||||
*/
|
||||
|
||||
extern crate serde_json;
|
||||
extern crate juniper;
|
||||
extern crate urlencoded;
|
||||
#[macro_use]
|
||||
extern crate iron;
|
||||
#[cfg(test)]
|
||||
extern crate iron_test;
|
||||
|
||||
use iron::prelude::*;
|
||||
use iron::middleware::Handler;
|
||||
use iron::mime::Mime;
|
||||
use iron::status;
|
||||
use iron::method;
|
||||
use urlencoded::{UrlDecodingError, UrlEncodedQuery};
|
||||
|
||||
use std::io::Read;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
use serde_json::error::Error as SerdeError;
|
||||
|
||||
use juniper::{GraphQLType, InputValue, RootNode};
|
||||
use juniper::http;
|
||||
|
||||
/// Handler that executes GraphQL queries in the given schema
|
||||
///
|
||||
/// The handler responds to GET requests and POST requests only. In GET
|
||||
/// requests, the query should be supplied in the `query` URL parameter, e.g.
|
||||
/// `http://localhost:3000/graphql?query={hero{name}}`.
|
||||
///
|
||||
/// POST requests support both queries and variables. POST a JSON document to
|
||||
/// this endpoint containing the field `"query"` and optionally `"variables"`.
|
||||
/// The variables should be a JSON object containing the variable to value
|
||||
/// mapping.
|
||||
pub struct GraphQLHandler<'a, CtxFactory, Query, Mutation, CtxT>
|
||||
where
|
||||
CtxFactory: Fn(&mut Request) -> CtxT + Send + Sync + 'static,
|
||||
CtxT: 'static,
|
||||
Query: GraphQLType<Context = CtxT> + Send + Sync + 'static,
|
||||
Mutation: GraphQLType<Context = CtxT> + Send + Sync + 'static,
|
||||
{
|
||||
context_factory: CtxFactory,
|
||||
root_node: RootNode<'a, Query, Mutation>,
|
||||
}
|
||||
|
||||
/// Handler that renders GraphiQL - a graphical query editor interface
|
||||
pub struct GraphiQLHandler {
|
||||
graphql_url: String,
|
||||
}
|
||||
|
||||
|
||||
fn get_single_value<T>(mut values: Vec<T>) -> IronResult<T> {
|
||||
if values.len() == 1 {
|
||||
Ok(values.remove(0))
|
||||
} else {
|
||||
Err(
|
||||
GraphQLIronError::InvalidData("Duplicate URL query parameter").into(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_url_param(params: Option<Vec<String>>) -> IronResult<Option<String>> {
|
||||
if let Some(values) = params {
|
||||
get_single_value(values).map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_variable_param(params: Option<Vec<String>>) -> IronResult<Option<InputValue>> {
|
||||
if let Some(values) = params {
|
||||
Ok(serde_json::from_str::<InputValue>(
|
||||
get_single_value(values)?.as_ref(),
|
||||
).map(Some)
|
||||
.map_err(GraphQLIronError::Serde)?)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl<'a, CtxFactory, Query, Mutation, CtxT> GraphQLHandler<'a, CtxFactory, Query, Mutation, CtxT>
|
||||
where
|
||||
CtxFactory: Fn(&mut Request) -> CtxT + Send + Sync + 'static,
|
||||
CtxT: 'static,
|
||||
Query: GraphQLType<Context = CtxT> + Send + Sync + 'static,
|
||||
Mutation: GraphQLType<Context = CtxT> + Send + Sync + 'static,
|
||||
{
|
||||
/// Build a new GraphQL handler
|
||||
///
|
||||
/// The context factory will receive the Iron request object and is
|
||||
/// expected to construct a context object for the given schema. This can
|
||||
/// be used to construct e.g. database connections or similar data that
|
||||
/// the schema needs to execute the query.
|
||||
pub fn new(context_factory: CtxFactory, query: Query, mutation: Mutation) -> Self {
|
||||
GraphQLHandler {
|
||||
context_factory: context_factory,
|
||||
root_node: RootNode::new(query, mutation),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn handle_get(&self, req: &mut Request) -> IronResult<http::GraphQLRequest> {
|
||||
let url_query_string = req.get_mut::<UrlEncodedQuery>()
|
||||
.map_err(|e| GraphQLIronError::Url(e))?;
|
||||
|
||||
let input_query = parse_url_param(url_query_string.remove("query"))?
|
||||
.ok_or_else(|| GraphQLIronError::InvalidData("No query provided"))?;
|
||||
let operation_name = parse_url_param(url_query_string.remove("operationName"))?;
|
||||
let variables = parse_variable_param(url_query_string.remove("variables"))?;
|
||||
|
||||
Ok(http::GraphQLRequest::new(
|
||||
input_query,
|
||||
operation_name,
|
||||
variables,
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_post(&self, req: &mut Request) -> IronResult<http::GraphQLRequest> {
|
||||
let mut request_payload = String::new();
|
||||
itry!(req.body.read_to_string(&mut request_payload));
|
||||
|
||||
Ok(serde_json::from_str::<http::GraphQLRequest>(
|
||||
request_payload.as_str(),
|
||||
).map_err(|err| GraphQLIronError::Serde(err))?)
|
||||
}
|
||||
|
||||
fn execute(&self, context: &CtxT, request: http::GraphQLRequest) -> IronResult<Response> {
|
||||
let response = request.execute(&self.root_node, context);
|
||||
let content_type = "application/json".parse::<Mime>().unwrap();
|
||||
let json = serde_json::to_string_pretty(&response).unwrap();
|
||||
let status = if response.is_ok() {
|
||||
status::Ok
|
||||
} else {
|
||||
status::BadRequest
|
||||
};
|
||||
Ok(Response::with((content_type, status, json)))
|
||||
}
|
||||
}
|
||||
|
||||
impl GraphiQLHandler {
|
||||
/// Build a new GraphiQL handler targeting the specified URL.
|
||||
///
|
||||
/// The provided URL should point to the URL of the attached `GraphQLHandler`. It can be
|
||||
/// relative, so a common value could be `"/graphql"`.
|
||||
pub fn new(graphql_url: &str) -> GraphiQLHandler {
|
||||
GraphiQLHandler {
|
||||
graphql_url: graphql_url.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, CtxFactory, Query, Mutation, CtxT> Handler
|
||||
for GraphQLHandler<'a, CtxFactory, Query, Mutation, CtxT>
|
||||
where
|
||||
CtxFactory: Fn(&mut Request) -> CtxT + Send + Sync + 'static,
|
||||
CtxT: 'static,
|
||||
Query: GraphQLType<Context = CtxT> + Send + Sync + 'static,
|
||||
Mutation: GraphQLType<Context = CtxT> + Send + Sync + 'static,
|
||||
'a: 'static,
|
||||
{
|
||||
fn handle(&self, mut req: &mut Request) -> IronResult<Response> {
|
||||
let context = (self.context_factory)(req);
|
||||
|
||||
let graphql_request = match req.method {
|
||||
method::Get => self.handle_get(&mut req)?,
|
||||
method::Post => self.handle_post(&mut req)?,
|
||||
_ => return Ok(Response::with((status::MethodNotAllowed))),
|
||||
};
|
||||
|
||||
self.execute(&context, graphql_request)
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler for GraphiQLHandler {
|
||||
fn handle(&self, _: &mut Request) -> IronResult<Response> {
|
||||
let content_type = "text/html".parse::<Mime>().unwrap();
|
||||
|
||||
Ok(Response::with((
|
||||
content_type,
|
||||
status::Ok,
|
||||
juniper::graphiql::graphiql_source(&self.graphql_url),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum GraphQLIronError {
|
||||
Serde(SerdeError),
|
||||
Url(UrlDecodingError),
|
||||
InvalidData(&'static str),
|
||||
}
|
||||
|
||||
impl fmt::Display for GraphQLIronError {
|
||||
fn fmt(&self, mut f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
GraphQLIronError::Serde(ref err) => fmt::Display::fmt(err, &mut f),
|
||||
GraphQLIronError::Url(ref err) => fmt::Display::fmt(err, &mut f),
|
||||
GraphQLIronError::InvalidData(ref err) => fmt::Display::fmt(err, &mut f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for GraphQLIronError {
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
GraphQLIronError::Serde(ref err) => err.description(),
|
||||
GraphQLIronError::Url(ref err) => err.description(),
|
||||
GraphQLIronError::InvalidData(ref err) => err,
|
||||
}
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&Error> {
|
||||
match *self {
|
||||
GraphQLIronError::Serde(ref err) => Some(err),
|
||||
GraphQLIronError::Url(ref err) => Some(err),
|
||||
GraphQLIronError::InvalidData(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GraphQLIronError> for IronError {
|
||||
fn from(err: GraphQLIronError) -> IronError {
|
||||
let message = format!("{}", err);
|
||||
IronError::new(err, (status::BadRequest, message))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use iron::prelude::*;
|
||||
use iron_test::{request, response};
|
||||
use iron::{Handler, Headers};
|
||||
|
||||
use juniper::tests::model::Database;
|
||||
use http::tests as http_tests;
|
||||
use juniper::EmptyMutation;
|
||||
|
||||
use super::GraphQLHandler;
|
||||
|
||||
struct TestIronIntegration;
|
||||
|
||||
impl http_tests::HTTPIntegration for TestIronIntegration {
|
||||
fn get(&self, url: &str) -> http_tests::TestResponse {
|
||||
make_test_response(request::get(
|
||||
&("http://localhost:3000".to_owned() + url),
|
||||
Headers::new(),
|
||||
&make_handler(),
|
||||
))
|
||||
}
|
||||
|
||||
fn post(&self, url: &str, body: &str) -> http_tests::TestResponse {
|
||||
make_test_response(request::post(
|
||||
&("http://localhost:3000".to_owned() + url),
|
||||
Headers::new(),
|
||||
body,
|
||||
&make_handler(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iron_integration() {
|
||||
let integration = TestIronIntegration;
|
||||
|
||||
http_tests::run_http_test_suite(&integration);
|
||||
}
|
||||
|
||||
fn context_factory(_: &mut Request) -> Database {
|
||||
Database::new()
|
||||
}
|
||||
|
||||
fn make_test_response(response: IronResult<Response>) -> http_tests::TestResponse {
|
||||
let response = response.expect("Error response from GraphQL handler");
|
||||
let status_code = response
|
||||
.status
|
||||
.expect("No status code returned from handler")
|
||||
.to_u16() as i32;
|
||||
let content_type = String::from_utf8(
|
||||
response
|
||||
.headers
|
||||
.get_raw("content-type")
|
||||
.expect("No content type header from handler")[0]
|
||||
.clone(),
|
||||
).expect("Content-type header invalid UTF-8");
|
||||
let body = response::extract_body_to_string(response);
|
||||
|
||||
http_tests::TestResponse {
|
||||
status_code: status_code,
|
||||
body: Some(body),
|
||||
content_type: content_type,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_handler() -> Box<Handler> {
|
||||
Box::new(GraphQLHandler::new(
|
||||
context_factory,
|
||||
Database::new(),
|
||||
EmptyMutation::<Database>::new(),
|
||||
))
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
[package]
|
||||
name = "juniper_rocket"
|
||||
version = "0.0.1"
|
||||
authors = ["Magnus Hallin <mhallin@fastmail.com>"]
|
||||
description = "Juniper GraphQL integration with Rocket"
|
||||
license = "BSD-2-Clause"
|
||||
documentation = "https://docs.rs/juniper_rocket"
|
||||
repository = "https://github.com/mhallin/juniper"
|
||||
|
||||
[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", path = "../juniper" }
|
||||
juniper_codegen = { version = "0.8.1", path = "../juniper_codegen" }
|
||||
|
||||
[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"] }
|
|
@ -1,41 +0,0 @@
|
|||
[tasks.build-verbose]
|
||||
command = "cargo"
|
||||
args = ["build", "--verbose"]
|
||||
condition_script = [
|
||||
'''
|
||||
if [ "$CARGO_MAKE_RUST_CHANNEL" = "nightly" ]; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
'''
|
||||
]
|
||||
|
||||
[tasks.build-verbose.windows]
|
||||
command = "cargo"
|
||||
args = ["build", "--verbose"]
|
||||
condition_script = [
|
||||
'''IF "%CARGO_MAKE_RUST_CHANNEL%"=="nightly" (exit 0) ELSE (exit 1)'''
|
||||
]
|
||||
|
||||
[tasks.test-verbose]
|
||||
command = "cargo"
|
||||
args = ["test", "--verbose"]
|
||||
dependencies = ["build-verbose"]
|
||||
condition_script = [
|
||||
'''
|
||||
if [ "$CARGO_MAKE_RUST_CHANNEL" = "nightly" ]; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
'''
|
||||
]
|
||||
|
||||
[tasks.test-verbose.windows]
|
||||
command = "cargo"
|
||||
args = ["test", "--verbose"]
|
||||
dependencies = ["build-verbose"]
|
||||
condition_script = [
|
||||
'''IF "%CARGO_MAKE_RUST_CHANNEL%"=="nightly" (exit 0) ELSE (exit 1)'''
|
||||
]
|
|
@ -1,51 +0,0 @@
|
|||
#![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<Database>>;
|
||||
|
||||
#[get("/")]
|
||||
fn graphiql() -> content::Html<String> {
|
||||
juniper_rocket::graphiql_source("/graphql")
|
||||
}
|
||||
|
||||
#[get("/graphql?<request>")]
|
||||
fn get_graphql_handler(
|
||||
context: State<Database>,
|
||||
request: juniper_rocket::GraphQLRequest,
|
||||
schema: State<Schema>,
|
||||
) -> juniper_rocket::GraphQLResponse {
|
||||
request.execute(&schema, &context)
|
||||
}
|
||||
|
||||
#[post("/graphql", data = "<request>")]
|
||||
fn post_graphql_handler(
|
||||
context: State<Database>,
|
||||
request: juniper_rocket::GraphQLRequest,
|
||||
schema: State<Schema>,
|
||||
) -> juniper_rocket::GraphQLResponse {
|
||||
request.execute(&schema, &context)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
rocket::ignite()
|
||||
.manage(Database::new())
|
||||
.manage(Schema::new(
|
||||
Database::new(),
|
||||
EmptyMutation::<Database>::new(),
|
||||
))
|
||||
.mount(
|
||||
"/",
|
||||
routes![graphiql, get_graphql_handler, post_graphql_handler],
|
||||
)
|
||||
.launch();
|
||||
}
|
|
@ -1,232 +0,0 @@
|
|||
#![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<String> {
|
||||
content::Html(juniper::graphiql::graphiql_source(graphql_endpoint_url))
|
||||
}
|
||||
|
||||
impl GraphQLRequest {
|
||||
/// Execute an incoming GraphQL query
|
||||
pub fn execute<CtxT, QueryT, MutationT>(
|
||||
&self,
|
||||
root_node: &RootNode<QueryT, MutationT>,
|
||||
context: &CtxT,
|
||||
) -> GraphQLResponse
|
||||
where
|
||||
QueryT: GraphQLType<Context = CtxT>,
|
||||
MutationT: GraphQLType<Context = CtxT>,
|
||||
{
|
||||
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<Self, String> {
|
||||
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::<InputValue>(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<Self, Self::Error> {
|
||||
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<Response<'r>, 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<Database>>;
|
||||
|
||||
|
||||
#[get("/?<request>")]
|
||||
fn get_graphql_handler(
|
||||
context: State<Database>,
|
||||
request: super::GraphQLRequest,
|
||||
schema: State<Schema>,
|
||||
) -> super::GraphQLResponse {
|
||||
request.execute(&schema, &context)
|
||||
}
|
||||
|
||||
#[post("/", data = "<request>")]
|
||||
fn post_graphql_handler(
|
||||
context: State<Database>,
|
||||
request: super::GraphQLRequest,
|
||||
schema: State<Schema>,
|
||||
) -> 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::<Database>::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::<Vec<_>>().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,
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
Loading…
Reference in a new issue