diff --git a/README.md b/README.md index ce5bfc11..3444d299 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,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 [Hyper][hyper], [Iron][iron], [Rocket], and [Warp][warp] frameworks, including -embedded [Graphiql][graphiql] for easy debugging. +embedded [Graphiql][graphiql] and [GraphQL Playground][playground] for easy debugging. - [Cargo crate](https://crates.io/crates/juniper) - [API Reference][docsrs] @@ -83,6 +83,7 @@ Juniper has not reached 1.0 yet, thus some API instability should be expected. [graphql]: http://graphql.org [graphiql]: https://github.com/graphql/graphiql +[playground]: https://github.com/prisma/graphql-playground [iron]: http://ironframework.io [graphql_spec]: http://facebook.github.io/graphql [test_schema_rs]: https://github.com/graphql-rust/juniper/blob/master/juniper/src/tests/schema.rs diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 891e64c4..42e54596 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -3,8 +3,9 @@ - The minimum required Rust version is now `1.30.0`. - The `ScalarValue` custom derive has been renamed to `GraphQLScalarValue`. - Fix introspection query validity - The DirectiveLocation::InlineFragment had an invalid literal value, - which broke third party tools like apollo cli. + The DirectiveLocation::InlineFragment had an invalid literal value, + which broke third party tools like apollo cli. +- Added GraphQL Playground integration # [0.11.1] 2018-12-19 diff --git a/juniper/src/http/mod.rs b/juniper/src/http/mod.rs index d24695d6..0759872a 100644 --- a/juniper/src/http/mod.rs +++ b/juniper/src/http/mod.rs @@ -1,6 +1,7 @@ //! Utilities for building HTTP endpoints in a library-agnostic manner pub mod graphiql; +pub mod playground; use serde::de::Deserialize; use serde::ser::{self, Serialize, SerializeMap}; diff --git a/juniper/src/http/playground.rs b/juniper/src/http/playground.rs new file mode 100644 index 00000000..57965686 --- /dev/null +++ b/juniper/src/http/playground.rs @@ -0,0 +1,546 @@ +//! Utility module to generate a GraphQL Playground interface + +/// Generate the HTML source to show a GraphQL Playground interface +// source: https://github.com/prisma/graphql-playground/blob/master/packages/graphql-playground-html/withAnimation.html +pub fn playground_source(graphql_endpoint_url: &str) -> String { + r##" + + + + + + + + GraphQL Playground + + + + + + + + + + +
+ +
Loading + GraphQL Playground +
+
+ +
+ + + + "##.replace("JUNIPER_GRAPHQL_URL", graphql_endpoint_url) +} diff --git a/juniper_hyper/src/lib.rs b/juniper_hyper/src/lib.rs index 700a9c92..eaa23dc0 100644 --- a/juniper_hyper/src/lib.rs +++ b/juniper_hyper/src/lib.rs @@ -93,6 +93,16 @@ pub fn graphiql( future::ok(resp) } +pub fn playground( + graphql_endpoint: &str, +) -> impl Future, Error = hyper::Error> { + let mut resp = new_html_response(StatusCode::OK); + *resp.body_mut() = Body::from(juniper::http::playground::playground_source( + graphql_endpoint, + )); + future::ok(resp) +} + fn render_error(err: GraphQLRequestError) -> Response { let message = format!("{}", err); let mut resp = new_response(StatusCode::BAD_REQUEST); diff --git a/juniper_iron/src/lib.rs b/juniper_iron/src/lib.rs index 9bff5a3b..bff305fa 100644 --- a/juniper_iron/src/lib.rs +++ b/juniper_iron/src/lib.rs @@ -221,6 +221,11 @@ pub struct GraphiQLHandler { graphql_url: String, } +/// Handler that renders `GraphQL Playground` - a graphical query editor interface +pub struct PlaygroundHandler { + graphql_url: String, +} + fn get_single_value(mut values: Vec) -> IronResult { if values.len() == 1 { Ok(values.remove(0)) @@ -327,6 +332,18 @@ impl GraphiQLHandler { } } +impl PlaygroundHandler { + /// Build a new GraphQL Playground 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) -> PlaygroundHandler { + PlaygroundHandler { + graphql_url: graphql_url.to_owned(), + } + } +} + impl<'a, CtxFactory, Query, Mutation, CtxT, S> Handler for GraphQLHandler<'a, CtxFactory, Query, Mutation, CtxT, S> where @@ -363,6 +380,18 @@ impl Handler for GraphiQLHandler { } } +impl Handler for PlaygroundHandler { + fn handle(&self, _: &mut Request) -> IronResult { + let content_type = "text/html; charset=utf-8".parse::().unwrap(); + + Ok(Response::with(( + content_type, + status::Ok, + juniper::http::playground::playground_source(&self.graphql_url), + ))) + } +} + #[derive(Debug)] enum GraphQLIronError { Serde(SerdeError), diff --git a/juniper_rocket/src/lib.rs b/juniper_rocket/src/lib.rs index ca5a7f69..b69cb983 100644 --- a/juniper_rocket/src/lib.rs +++ b/juniper_rocket/src/lib.rs @@ -147,6 +147,13 @@ pub fn graphiql_source(graphql_endpoint_url: &str) -> content::Html { content::Html(juniper::graphiql::graphiql_source(graphql_endpoint_url)) } +/// Generate an HTML page containing GraphQL Playground +pub fn playground_source(graphql_endpoint_url: &str) -> content::Html { + content::Html(juniper::http::playground::playground_source( + graphql_endpoint_url, + )) +} + impl GraphQLRequest where S: ScalarValue, diff --git a/juniper_warp/src/lib.rs b/juniper_warp/src/lib.rs index cab91c06..0a24054a 100644 --- a/juniper_warp/src/lib.rs +++ b/juniper_warp/src/lib.rs @@ -298,6 +298,22 @@ fn graphiql_response(graphql_endpoint_url: &'static str) -> warp::http::Response .expect("response is valid") } +/// Create a filter that replies with an HTML page containing GraphQL Playground. This does not handle routing, so you can mount it on any endpoint. +pub fn playground_filter( + graphql_endpoint_url: &'static str, +) -> warp::filters::BoxedFilter<(warp::http::Response>,)> { + warp::any() + .map(move || playground_response(graphql_endpoint_url)) + .boxed() +} + +fn playground_response(graphql_endpoint_url: &'static str) -> warp::http::Response> { + warp::http::Response::builder() + .header("content-type", "text/html;charset=utf-8") + .body(juniper::http::playground::playground_source(graphql_endpoint_url).into_bytes()) + .expect("response is valid") +} + #[cfg(test)] mod tests { use super::*; @@ -345,6 +361,42 @@ mod tests { assert!(body.contains("")); } + #[test] + fn playground_endpoint_matches() { + let filter = warp::get2() + .and(warp::path("playground")) + .and(playground_filter("/graphql")); + let result = request() + .method("GET") + .path("/playground") + .header("accept", "text/html") + .filter(&filter); + + assert!(result.is_ok()); + } + + #[test] + fn playground_endpoint_returns_playground_source() { + let filter = warp::get2() + .and(warp::path("dogs-api")) + .and(warp::path("playground")) + .and(playground_filter("/dogs-api/graphql")); + let response = request() + .method("GET") + .path("/dogs-api/playground") + .header("accept", "text/html") + .reply(&filter); + + assert_eq!(response.status(), http::StatusCode::OK); + assert_eq!( + response.headers().get("content-type").unwrap(), + "text/html;charset=utf-8" + ); + let body = String::from_utf8(response.body().to_vec()).unwrap(); + + assert!(body.contains("GraphQLPlayground.init(root, { endpoint: '/dogs-api/graphql' })")); + } + #[test] fn graphql_handler_works_json_post() { use juniper::tests::model::Database;