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;