Add GraphQL Playground integration (#317)

This commit is contained in:
Jakob Gillich 2019-01-26 05:58:01 +01:00 committed by Christian Legnitto
parent 29389e8f72
commit b4a0669b37
8 changed files with 650 additions and 3 deletions

View file

@ -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 Juniper does not include a web server - instead it provides building blocks to
make integration with existing servers straightforward. It optionally provides a 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 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) - [Cargo crate](https://crates.io/crates/juniper)
- [API Reference][docsrs] - [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 [graphql]: http://graphql.org
[graphiql]: https://github.com/graphql/graphiql [graphiql]: https://github.com/graphql/graphiql
[playground]: https://github.com/prisma/graphql-playground
[iron]: http://ironframework.io [iron]: http://ironframework.io
[graphql_spec]: http://facebook.github.io/graphql [graphql_spec]: http://facebook.github.io/graphql
[test_schema_rs]: https://github.com/graphql-rust/juniper/blob/master/juniper/src/tests/schema.rs [test_schema_rs]: https://github.com/graphql-rust/juniper/blob/master/juniper/src/tests/schema.rs

View file

@ -3,8 +3,9 @@
- The minimum required Rust version is now `1.30.0`. - The minimum required Rust version is now `1.30.0`.
- The `ScalarValue` custom derive has been renamed to `GraphQLScalarValue`. - The `ScalarValue` custom derive has been renamed to `GraphQLScalarValue`.
- Fix introspection query validity - Fix introspection query validity
The DirectiveLocation::InlineFragment had an invalid literal value, The DirectiveLocation::InlineFragment had an invalid literal value,
which broke third party tools like apollo cli. which broke third party tools like apollo cli.
- Added GraphQL Playground integration
# [0.11.1] 2018-12-19 # [0.11.1] 2018-12-19

View file

@ -1,6 +1,7 @@
//! Utilities for building HTTP endpoints in a library-agnostic manner //! Utilities for building HTTP endpoints in a library-agnostic manner
pub mod graphiql; pub mod graphiql;
pub mod playground;
use serde::de::Deserialize; use serde::de::Deserialize;
use serde::ser::{self, Serialize, SerializeMap}; use serde::ser::{self, Serialize, SerializeMap};

View file

@ -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)
}

View file

@ -93,6 +93,16 @@ pub fn graphiql(
future::ok(resp) 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> { fn render_error(err: GraphQLRequestError) -> Response<Body> {
let message = format!("{}", err); let message = format!("{}", err);
let mut resp = new_response(StatusCode::BAD_REQUEST); let mut resp = new_response(StatusCode::BAD_REQUEST);

View file

@ -221,6 +221,11 @@ pub struct GraphiQLHandler {
graphql_url: String, 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> { fn get_single_value<T>(mut values: Vec<T>) -> IronResult<T> {
if values.len() == 1 { if values.len() == 1 {
Ok(values.remove(0)) 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 impl<'a, CtxFactory, Query, Mutation, CtxT, S> Handler
for GraphQLHandler<'a, CtxFactory, Query, Mutation, CtxT, S> for GraphQLHandler<'a, CtxFactory, Query, Mutation, CtxT, S>
where 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)] #[derive(Debug)]
enum GraphQLIronError { enum GraphQLIronError {
Serde(SerdeError), Serde(SerdeError),

View file

@ -147,6 +147,13 @@ pub fn graphiql_source(graphql_endpoint_url: &str) -> content::Html<String> {
content::Html(juniper::graphiql::graphiql_source(graphql_endpoint_url)) 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> impl<S> GraphQLRequest<S>
where where
S: ScalarValue, S: ScalarValue,

View file

@ -298,6 +298,22 @@ fn graphiql_response(graphql_endpoint_url: &'static str) -> warp::http::Response
.expect("response is valid") .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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -345,6 +361,42 @@ mod tests {
assert!(body.contains("<script>var GRAPHQL_URL = '/dogs-api/graphql';</script>")); 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] #[test]
fn graphql_handler_works_json_post() { fn graphql_handler_works_json_post() {
use juniper::tests::model::Database; use juniper::tests::model::Database;