Repository: graphql-rust/juniper (language: rust)

Issue:
Add playground integration?
I saw in #230 playground integration was removed along the way. Is that just because it would need to be refactored into a central `playground_source` function similar to [`graphiql_source`](https://github.com/graphql-rust/juniper/blob/4be687f73eeb8c80044f542dfea49910e8347a71/juniper/src/http/graphiql.rs#L4-L66) or because of some other reason?

Playground has a lot more features than graphiql, like the ability to set/copy http headers (which is necessary for many APIs), so it would be nice to have... 

Although it looks like juniper's graphiql implementation allows sending the auth header? It still would be nice to have playground support out of the box.


Discussion / hints:
Yep! Happy to take a PR that adds it.

Fix patch:
diff --git a/README.md b/README.md
index ce5bfc11d..3444d2991 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 891e64c40..42e545969 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 d24695d6d..0759872a8 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 000000000..579656860
--- /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##"
+<!DOCTYPE html>
+
+<html>
+
+<head>
+  <meta charset=utf-8 />
+  <meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui">
+  <title>GraphQL Playground</title>
+  <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/graphql-playground-react@1.7.11/build/static/css/index.css" />
+  <link rel="shortcut icon" href="//cdn.jsdelivr.net/npm/graphql-playground-react@1.7.11/build/favicon.png" />
+  <script src="//cdn.jsdelivr.net/npm/graphql-playground-react@1.7.11/build/static/js/middleware.js"></script>
+
+</head>
+
+<body>
+  <style type="text/css">
+    html {
+      font-family: "Open Sans", sans-serif;
+      overflow: hidden;
+    }
+
+    body {
+      margin: 0;
+      background: #172a3a;
+    }
+
+    .playgroundIn {
+      -webkit-animation: playgroundIn 0.5s ease-out forwards;
+      animation: playgroundIn 0.5s ease-out forwards;
+    }
+
+    @-webkit-keyframes playgroundIn {
+      from {
+        opacity: 0;
+        -webkit-transform: translateY(10px);
+        -ms-transform: translateY(10px);
+        transform: translateY(10px);
+      }
+      to {
+        opacity: 1;
+        -webkit-transform: translateY(0);
+        -ms-transform: translateY(0);
+        transform: translateY(0);
+      }
+    }
+
+    @keyframes playgroundIn {
+      from {
+        opacity: 0;
+        -webkit-transform: translateY(10px);
+        -ms-transform: translateY(10px);
+        transform: translateY(10px);
+      }
+      to {
+        opacity: 1;
+        -webkit-transform: translateY(0);
+        -ms-transform: translateY(0);
+        transform: translateY(0);
+      }
+    }
+  </style>
+
+  <style type="text/css">
+    .fadeOut {
+      -webkit-animation: fadeOut 0.5s ease-out forwards;
+      animation: fadeOut 0.5s ease-out forwards;
+    }
+
+    @-webkit-keyframes fadeIn {
+      from {
+        opacity: 0;
+        -webkit-transform: translateY(-10px);
+        -ms-transform: translateY(-10px);
+        transform: translateY(-10px);
+      }
+      to {
+        opacity: 1;
+        -webkit-transform: translateY(0);
+        -ms-transform: translateY(0);
+        transform: translateY(0);
+      }
+    }
+
+    @keyframes fadeIn {
+      from {
+        opacity: 0;
+        -webkit-transform: translateY(-10px);
+        -ms-transform: translateY(-10px);
+        transform: translateY(-10px);
+      }
+      to {
+        opacity: 1;
+        -webkit-transform: translateY(0);
+        -ms-transform: translateY(0);
+        transform: translateY(0);
+      }
+    }
+
+    @-webkit-keyframes fadeOut {
+      from {
+        opacity: 1;
+        -webkit-transform: translateY(0);
+        -ms-transform: translateY(0);
+        transform: translateY(0);
+      }
+      to {
+        opacity: 0;
+        -webkit-transform: translateY(-10px);
+        -ms-transform: translateY(-10px);
+        transform: translateY(-10px);
+      }
+    }
+
+    @keyframes fadeOut {
+      from {
+        opacity: 1;
+        -webkit-transform: translateY(0);
+        -ms-transform: translateY(0);
+        transform: translateY(0);
+      }
+      to {
+        opacity: 0;
+        -webkit-transform: translateY(-10px);
+        -ms-transform: translateY(-10px);
+        transform: translateY(-10px);
+      }
+    }
+
+    @-webkit-keyframes appearIn {
+      from {
+        opacity: 0;
+        -webkit-transform: translateY(0px);
+        -ms-transform: translateY(0px);
+        transform: translateY(0px);
+      }
+      to {
+        opacity: 1;
+        -webkit-transform: translateY(0);
+        -ms-transform: translateY(0);
+        transform: translateY(0);
+      }
+    }
+
+    @keyframes appearIn {
+      from {
+        opacity: 0;
+        -webkit-transform: translateY(0px);
+        -ms-transform: translateY(0px);
+        transform: translateY(0px);
+      }
+      to {
+        opacity: 1;
+        -webkit-transform: translateY(0);
+        -ms-transform: translateY(0);
+        transform: translateY(0);
+      }
+    }
+
+    @-webkit-keyframes scaleIn {
+      from {
+        -webkit-transform: scale(0);
+        -ms-transform: scale(0);
+        transform: scale(0);
+      }
+      to {
+        -webkit-transform: scale(1);
+        -ms-transform: scale(1);
+        transform: scale(1);
+      }
+    }
+
+    @keyframes scaleIn {
+      from {
+        -webkit-transform: scale(0);
+        -ms-transform: scale(0);
+        transform: scale(0);
+      }
+      to {
+        -webkit-transform: scale(1);
+        -ms-transform: scale(1);
+        transform: scale(1);
+      }
+    }
+
+    @-webkit-keyframes innerDrawIn {
+      0% {
+        stroke-dashoffset: 70;
+      }
+      50% {
+        stroke-dashoffset: 140;
+      }
+      100% {
+        stroke-dashoffset: 210;
+      }
+    }
+
+    @keyframes innerDrawIn {
+      0% {
+        stroke-dashoffset: 70;
+      }
+      50% {
+        stroke-dashoffset: 140;
+      }
+      100% {
+        stroke-dashoffset: 210;
+      }
+    }
+
+    @-webkit-keyframes outerDrawIn {
+      0% {
+        stroke-dashoffset: 76;
+      }
+      100% {
+        stroke-dashoffset: 152;
+      }
+    }
+
+    @keyframes outerDrawIn {
+      0% {
+        stroke-dashoffset: 76;
+      }
+      100% {
+        stroke-dashoffset: 152;
+      }
+    }
+
+    .hHWjkv {
+      -webkit-transform-origin: 0px 0px;
+      -ms-transform-origin: 0px 0px;
+      transform-origin: 0px 0px;
+      -webkit-transform: scale(0);
+      -ms-transform: scale(0);
+      transform: scale(0);
+      -webkit-animation: scaleIn 0.25s linear forwards 0.2222222222222222s;
+      animation: scaleIn 0.25s linear forwards 0.2222222222222222s;
+    }
+
+    .gCDOzd {
+      -webkit-transform-origin: 0px 0px;
+      -ms-transform-origin: 0px 0px;
+      transform-origin: 0px 0px;
+      -webkit-transform: scale(0);
+      -ms-transform: scale(0);
+      transform: scale(0);
+      -webkit-animation: scaleIn 0.25s linear forwards 0.4222222222222222s;
+      animation: scaleIn 0.25s linear forwards 0.4222222222222222s;
+    }
+
+    .hmCcxi {
+      -webkit-transform-origin: 0px 0px;
+      -ms-transform-origin: 0px 0px;
+      transform-origin: 0px 0px;
+      -webkit-transform: scale(0);
+      -ms-transform: scale(0);
+      transform: scale(0);
+      -webkit-animation: scaleIn 0.25s linear forwards 0.6222222222222222s;
+      animation: scaleIn 0.25s linear forwards 0.6222222222222222s;
+    }
+
+    .eHamQi {
+      -webkit-transform-origin: 0px 0px;
+      -ms-transform-origin: 0px 0px;
+      transform-origin: 0px 0px;
+      -webkit-transform: scale(0);
+      -ms-transform: scale(0);
+      transform: scale(0);
+      -webkit-animation: scaleIn 0.25s linear forwards 0.8222222222222223s;
+      animation: scaleIn 0.25s linear forwards 0.8222222222222223s;
+    }
+
+    .byhgGu {
+      -webkit-transform-origin: 0px 0px;
+      -ms-transform-origin: 0px 0px;
+      transform-origin: 0px 0px;
+      -webkit-transform: scale(0);
+      -ms-transform: scale(0);
+      transform: scale(0);
+      -webkit-animation: scaleIn 0.25s linear forwards 1.0222222222222221s;
+      animation: scaleIn 0.25s linear forwards 1.0222222222222221s;
+    }
+
+    .llAKP {
+      -webkit-transform-origin: 0px 0px;
+      -ms-transform-origin: 0px 0px;
+      transform-origin: 0px 0px;
+      -webkit-transform: scale(0);
+      -ms-transform: scale(0);
+      transform: scale(0);
+      -webkit-animation: scaleIn 0.25s linear forwards 1.2222222222222223s;
+      animation: scaleIn 0.25s linear forwards 1.2222222222222223s;
+    }
+
+    .bglIGM {
+      -webkit-transform-origin: 64px 28px;
+      -ms-transform-origin: 64px 28px;
+      transform-origin: 64px 28px;
+      -webkit-transform: scale(0);
+      -ms-transform: scale(0);
+      transform: scale(0);
+      -webkit-animation: scaleIn 0.25s linear forwards 0.2222222222222222s;
+      animation: scaleIn 0.25s linear forwards 0.2222222222222222s;
+    }
+
+    .ksxRII {
+      -webkit-transform-origin: 95.98500061035156px 46.510000228881836px;
+      -ms-transform-origin: 95.98500061035156px 46.510000228881836px;
+      transform-origin: 95.98500061035156px 46.510000228881836px;
+      -webkit-transform: scale(0);
+      -ms-transform: scale(0);
+      transform: scale(0);
+      -webkit-animation: scaleIn 0.25s linear forwards 0.4222222222222222s;
+      animation: scaleIn 0.25s linear forwards 0.4222222222222222s;
+    }
+
+    .cWrBmb {
+      -webkit-transform-origin: 95.97162628173828px 83.4900016784668px;
+      -ms-transform-origin: 95.97162628173828px 83.4900016784668px;
+      transform-origin: 95.97162628173828px 83.4900016784668px;
+      -webkit-transform: scale(0);
+      -ms-transform: scale(0);
+      transform: scale(0);
+      -webkit-animation: scaleIn 0.25s linear forwards 0.6222222222222222s;
+      animation: scaleIn 0.25s linear forwards 0.6222222222222222s;
+    }
+
+    .Wnusb {
+      -webkit-transform-origin: 64px 101.97999572753906px;
+      -ms-transform-origin: 64px 101.97999572753906px;
+      transform-origin: 64px 101.97999572753906px;
+      -webkit-transform: scale(0);
+      -ms-transform: scale(0);
+      transform: scale(0);
+      -webkit-animation: scaleIn 0.25s linear forwards 0.8222222222222223s;
+      animation: scaleIn 0.25s linear forwards 0.8222222222222223s;
+    }
+
+    .bfPqf {
+      -webkit-transform-origin: 32.03982162475586px 83.4900016784668px;
+      -ms-transform-origin: 32.03982162475586px 83.4900016784668px;
+      transform-origin: 32.03982162475586px 83.4900016784668px;
+      -webkit-transform: scale(0);
+      -ms-transform: scale(0);
+      transform: scale(0);
+      -webkit-animation: scaleIn 0.25s linear forwards 1.0222222222222221s;
+      animation: scaleIn 0.25s linear forwards 1.0222222222222221s;
+    }
+
+    .edRCTN {
+      -webkit-transform-origin: 32.033552169799805px 46.510000228881836px;
+      -ms-transform-origin: 32.033552169799805px 46.510000228881836px;
+      transform-origin: 32.033552169799805px 46.510000228881836px;
+      -webkit-transform: scale(0);
+      -ms-transform: scale(0);
+      transform: scale(0);
+      -webkit-animation: scaleIn 0.25s linear forwards 1.2222222222222223s;
+      animation: scaleIn 0.25s linear forwards 1.2222222222222223s;
+    }
+
+    .iEGVWn {
+      opacity: 0;
+      stroke-dasharray: 76;
+      -webkit-animation: outerDrawIn 0.5s ease-out forwards 0.3333333333333333s, appearIn 0.1s ease-out forwards 0.3333333333333333s;
+      animation: outerDrawIn 0.5s ease-out forwards 0.3333333333333333s, appearIn 0.1s ease-out forwards 0.3333333333333333s;
+      -webkit-animation-iteration-count: 1, 1;
+      animation-iteration-count: 1, 1;
+    }
+
+    .bsocdx {
+      opacity: 0;
+      stroke-dasharray: 76;
+      -webkit-animation: outerDrawIn 0.5s ease-out forwards 0.5333333333333333s, appearIn 0.1s ease-out forwards 0.5333333333333333s;
+      animation: outerDrawIn 0.5s ease-out forwards 0.5333333333333333s, appearIn 0.1s ease-out forwards 0.5333333333333333s;
+      -webkit-animation-iteration-count: 1, 1;
+      animation-iteration-count: 1, 1;
+    }
+
+    .jAZXmP {
+      opacity: 0;
+      stroke-dasharray: 76;
+      -webkit-animation: outerDrawIn 0.5s ease-out forwards 0.7333333333333334s, appearIn 0.1s ease-out forwards 0.7333333333333334s;
+      animation: outerDrawIn 0.5s ease-out forwards 0.7333333333333334s, appearIn 0.1s ease-out forwards 0.7333333333333334s;
+      -webkit-animation-iteration-count: 1, 1;
+      animation-iteration-count: 1, 1;
+    }
+
+    .hSeArx {
+      opacity: 0;
+      stroke-dasharray: 76;
+      -webkit-animation: outerDrawIn 0.5s ease-out forwards 0.9333333333333333s, appearIn 0.1s ease-out forwards 0.9333333333333333s;
+      animation: outerDrawIn 0.5s ease-out forwards 0.9333333333333333s, appearIn 0.1s ease-out forwards 0.9333333333333333s;
+      -webkit-animation-iteration-count: 1, 1;
+      animation-iteration-count: 1, 1;
+    }
+
+    .bVgqGk {
+      opacity: 0;
+      stroke-dasharray: 76;
+      -webkit-animation: outerDrawIn 0.5s ease-out forwards 1.1333333333333333s, appearIn 0.1s ease-out forwards 1.1333333333333333s;
+      animation: outerDrawIn 0.5s ease-out forwards 1.1333333333333333s, appearIn 0.1s ease-out forwards 1.1333333333333333s;
+      -webkit-animation-iteration-count: 1, 1;
+      animation-iteration-count: 1, 1;
+    }
+
+    .hEFqBt {
+      opacity: 0;
+      stroke-dasharray: 76;
+      -webkit-animation: outerDrawIn 0.5s ease-out forwards 1.3333333333333333s, appearIn 0.1s ease-out forwards 1.3333333333333333s;
+      animation: outerDrawIn 0.5s ease-out forwards 1.3333333333333333s, appearIn 0.1s ease-out forwards 1.3333333333333333s;
+      -webkit-animation-iteration-count: 1, 1;
+      animation-iteration-count: 1, 1;
+    }
+
+    .dzEKCM {
+      opacity: 0;
+      stroke-dasharray: 70;
+      -webkit-animation: innerDrawIn 1s ease-in-out forwards 1.3666666666666667s, appearIn 0.1s linear forwards 1.3666666666666667s;
+      animation: innerDrawIn 1s ease-in-out forwards 1.3666666666666667s, appearIn 0.1s linear forwards 1.3666666666666667s;
+      -webkit-animation-iteration-count: infinite, 1;
+      animation-iteration-count: infinite, 1;
+    }
+
+    .DYnPx {
+      opacity: 0;
+      stroke-dasharray: 70;
+      -webkit-animation: innerDrawIn 1s ease-in-out forwards 1.5333333333333332s, appearIn 0.1s linear forwards 1.5333333333333332s;
+      animation: innerDrawIn 1s ease-in-out forwards 1.5333333333333332s, appearIn 0.1s linear forwards 1.5333333333333332s;
+      -webkit-animation-iteration-count: infinite, 1;
+      animation-iteration-count: infinite, 1;
+    }
+
+    .hjPEAQ {
+      opacity: 0;
+      stroke-dasharray: 70;
+      -webkit-animation: innerDrawIn 1s ease-in-out forwards 1.7000000000000002s, appearIn 0.1s linear forwards 1.7000000000000002s;
+      animation: innerDrawIn 1s ease-in-out forwards 1.7000000000000002s, appearIn 0.1s linear forwards 1.7000000000000002s;
+      -webkit-animation-iteration-count: infinite, 1;
+      animation-iteration-count: infinite, 1;
+    }
+
+    #loading-wrapper {
+      position: absolute;
+      width: 100vw;
+      height: 100vh;
+      display: -webkit-box;
+      display: -webkit-flex;
+      display: -ms-flexbox;
+      display: flex;
+      -webkit-align-items: center;
+      -webkit-box-align: center;
+      -ms-flex-align: center;
+      align-items: center;
+      -webkit-box-pack: center;
+      -webkit-justify-content: center;
+      -ms-flex-pack: center;
+      justify-content: center;
+      -webkit-flex-direction: column;
+      -ms-flex-direction: column;
+      flex-direction: column;
+    }
+
+    .logo {
+      width: 75px;
+      height: 75px;
+      margin-bottom: 20px;
+      opacity: 0;
+      -webkit-animation: fadeIn 0.5s ease-out forwards;
+      animation: fadeIn 0.5s ease-out forwards;
+    }
+
+    .text {
+      font-size: 32px;
+      font-weight: 200;
+      text-align: center;
+      color: rgba(255, 255, 255, 0.6);
+      opacity: 0;
+      -webkit-animation: fadeIn 0.5s ease-out forwards;
+      animation: fadeIn 0.5s ease-out forwards;
+    }
+
+    .dGfHfc {
+      font-weight: 400;
+    }
+  </style>
+  <div id="loading-wrapper">
+    <svg class="logo" viewBox="0 0 128 128" xmlns:xlink="http://www.w3.org/1999/xlink">
+      <title>GraphQL Playground Logo</title>
+      <defs>
+        <linearGradient id="linearGradient-1" x1="4.86%" x2="96.21%" y1="0%" y2="99.66%">
+          <stop stop-color="#E00082" stop-opacity=".8" offset="0%"></stop>
+          <stop stop-color="#E00082" offset="100%"></stop>
+        </linearGradient>
+      </defs>
+      <g>
+        <rect id="Gradient" width="127.96" height="127.96" y="1" fill="url(#linearGradient-1)" rx="4"></rect>
+        <path id="Border" fill="#E00082" fill-rule="nonzero" d="M4.7 2.84c-1.58 0-2.86 1.28-2.86 2.85v116.57c0 1.57 1.28 2.84 2.85 2.84h116.57c1.57 0 2.84-1.26 2.84-2.83V5.67c0-1.55-1.26-2.83-2.83-2.83H4.67zM4.7 0h116.58c3.14 0 5.68 2.55 5.68 5.7v116.58c0 3.14-2.54 5.68-5.68 5.68H4.68c-3.13 0-5.68-2.54-5.68-5.68V5.68C-1 2.56 1.55 0 4.7 0z"></path>
+        <path class="bglIGM" x="64" y="28" fill="#fff" d="M64 36c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8" style="transform: translate(100px, 100px);"></path>
+        <path class="ksxRII" x="95.98500061035156" y="46.510000228881836" fill="#fff" d="M89.04 50.52c-2.2-3.84-.9-8.73 2.94-10.96 3.83-2.2 8.72-.9 10.95 2.94 2.2 3.84.9 8.73-2.94 10.96-3.85 2.2-8.76.9-10.97-2.94"
+          style="transform: translate(100px, 100px);"></path>
+        <path class="cWrBmb" x="95.97162628173828" y="83.4900016784668" fill="#fff" d="M102.9 87.5c-2.2 3.84-7.1 5.15-10.94 2.94-3.84-2.2-5.14-7.12-2.94-10.96 2.2-3.84 7.12-5.15 10.95-2.94 3.86 2.23 5.16 7.12 2.94 10.96"
+          style="transform: translate(100px, 100px);"></path>
+        <path class="Wnusb" x="64" y="101.97999572753906" fill="#fff" d="M64 110c-4.43 0-8-3.6-8-8.02 0-4.44 3.57-8.02 8-8.02s8 3.58 8 8.02c0 4.4-3.57 8.02-8 8.02"
+          style="transform: translate(100px, 100px);"></path>
+        <path class="bfPqf" x="32.03982162475586" y="83.4900016784668" fill="#fff" d="M25.1 87.5c-2.2-3.84-.9-8.73 2.93-10.96 3.83-2.2 8.72-.9 10.95 2.94 2.2 3.84.9 8.73-2.94 10.96-3.85 2.2-8.74.9-10.95-2.94"
+          style="transform: translate(100px, 100px);"></path>
+        <path class="edRCTN" x="32.033552169799805" y="46.510000228881836" fill="#fff" d="M38.96 50.52c-2.2 3.84-7.12 5.15-10.95 2.94-3.82-2.2-5.12-7.12-2.92-10.96 2.2-3.84 7.12-5.15 10.95-2.94 3.83 2.23 5.14 7.12 2.94 10.96"
+          style="transform: translate(100px, 100px);"></path>
+        <path class="iEGVWn" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M63.55 27.5l32.9 19-32.9-19z"></path>
+        <path class="bsocdx" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M96 46v38-38z"></path>
+        <path class="jAZXmP" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M96.45 84.5l-32.9 19 32.9-19z"></path>
+        <path class="hSeArx" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M64.45 103.5l-32.9-19 32.9 19z"></path>
+        <path class="bVgqGk" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M32 84V46v38z"></path>
+        <path class="hEFqBt" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M31.55 46.5l32.9-19-32.9 19z"></path>
+        <path class="dzEKCM" id="Triangle-Bottom" stroke="#fff" stroke-width="4" d="M30 84h70" stroke-linecap="round"></path>
+        <path class="DYnPx" id="Triangle-Left" stroke="#fff" stroke-width="4" d="M65 26L30 87" stroke-linecap="round"></path>
+        <path class="hjPEAQ" id="Triangle-Right" stroke="#fff" stroke-width="4" d="M98 87L63 26" stroke-linecap="round"></path>
+      </g>
+    </svg>
+    <div class="text">Loading
+      <span class="dGfHfc">GraphQL Playground</span>
+    </div>
+  </div>
+
+  <div id="root" />
+  <script type="text/javascript">
+    window.addEventListener('load', function (event) {
+
+      const loadingWrapper = document.getElementById('loading-wrapper');
+      loadingWrapper.classList.add('fadeOut');
+
+
+      const root = document.getElementById('root');
+      root.classList.add('playgroundIn');
+
+      GraphQLPlayground.init(root, { endpoint: 'JUNIPER_GRAPHQL_URL' })
+    })
+  </script>
+</body>
+</html>
+  "##.replace("JUNIPER_GRAPHQL_URL", graphql_endpoint_url)
+}
diff --git a/juniper_hyper/src/lib.rs b/juniper_hyper/src/lib.rs
index 700a9c925..eaa23dc04 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<Item = Response<Body>, 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<Body> {
     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 9bff5a3b5..bff305fad 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<T>(mut values: Vec<T>) -> IronResult<T> {
     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<Response> {
+        let content_type = "text/html; charset=utf-8".parse::<Mime>().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 ca5a7f690..b69cb9838 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<String> {
     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<String> {
+    content::Html(juniper::http::playground::playground_source(
+        graphql_endpoint_url,
+    ))
+}
+
 impl<S> GraphQLRequest<S>
 where
     S: ScalarValue,
diff --git a/juniper_warp/src/lib.rs b/juniper_warp/src/lib.rs
index cab91c065..0a24054a1 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<Vec<u8>>,)> {
+    warp::any()
+        .map(move || playground_response(graphql_endpoint_url))
+        .boxed()
+}
+
+fn playground_response(graphql_endpoint_url: &'static str) -> warp::http::Response<Vec<u8>> {
+    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("<script>var GRAPHQL_URL = '/dogs-api/graphql';</script>"));
     }
 
+    #[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;


Test patch: