Move juniper_iron crate back into the main repo.

This is done for a lower maintainance burden and combined testing.
This commit is contained in:
theduke 2017-12-02 15:55:29 +01:00
parent bdb1dd34af
commit bec5295827
8 changed files with 525 additions and 0 deletions

View file

@ -3,4 +3,5 @@ members = [
"juniper",
"juniper_codegen",
"juniper_tests",
"juniper_iron",
]

2
juniper_iron/.gitignore vendored Normal file
View file

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

View file

26
juniper_iron/Cargo.toml Normal file
View file

@ -0,0 +1,26 @@
[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/graphql-rust/juniper_iron"
[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", git = "https://github.com/graphql-rust/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", features = ["expose-test-schema", "serde_json"], git = "https://github.com/graphql-rust/juniper" }
[badges]
travis-ci = { repository = "graphql-rust/juniper_iron" }
appveyor = { repository = "graphql-rust/juniper_iron" }

25
juniper_iron/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.

31
juniper_iron/README.md Normal file
View file

@ -0,0 +1,31 @@
# juniper_iron
[![Build Status](https://travis-ci.org/graphql-rust/juniper_iron.svg?branch=master)](https://travis-ci.org/graphql-rust/juniper_iron)
[![Build status](https://ci.appveyor.com/api/projects/status/rqguvfkl9m0g7hum?svg=true)](https://ci.appveyor.com/project/theduke/juniper-iron)
[![Crates.io](https://img.shields.io/crates/v/juniper_iron.svg?maxAge=2592000)](https://crates.io/crates/juniper_iron)
[![Gitter chat](https://badges.gitter.im/juniper-graphql/gitter.png)](https://gitter.im/juniper-graphql)
This repository contains the [Iron][Iron] web framework integration for [Juniper][Juniper], a [GraphQL][GraphQL]
implementation for Rust.
## Documentation
Once the crate is published, documentation will be on [docs.rs][documentation].
For now, please consult the documentation comments [here](https://github.com/graphql-rust/juniper_iron/blob/master/src/lib.rs).
## Examples
Check [examples/iron_server.rs][example] for example code of a working Iron server with GraphQL handlers.
## License
This project is under the BSD-2 license.
Check the LICENSE file for details.
[Iron]: https://github.com/iron/iron
[Juniper]: https://github.com/graphql-rust/juniper
[GraphQL]: http://graphql.org
[documentation]: https://docs.rs/juniper_iron
[example]: https://github.com/graphql-rust/juniper_iron/blob/master/examples/iron_server.rs

View file

@ -0,0 +1,43 @@
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();
}

397
juniper_iron/src/lib.rs Normal file
View file

@ -0,0 +1,397 @@
/*!
[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, TypeInfo=()> + Send + Sync + 'static,
Mutation: GraphQLType<Context = CtxT, TypeInfo=()> + 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, TypeInfo=()> + Send + Sync + 'static,
Mutation: GraphQLType<Context = CtxT, TypeInfo=()> + 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 juniper::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(),
))
}
}