Skip to content

Commit 10dadd7

Browse files
committed
Implementation of integration between actix-web and juniper
1 parent c12f40c commit 10dadd7

File tree

30 files changed

+1054
-550
lines changed

30 files changed

+1054
-550
lines changed

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ members = [
99
"juniper_hyper",
1010
"juniper_iron",
1111
"juniper_rocket",
12+
"juniper_rocket_async",
1213
"juniper_subscriptions",
1314
"juniper_warp",
15+
"juniper_actix",
1416
]
1517
exclude = [
1618
"docs/book/tests",
1719
"examples/warp_async",
1820
"examples/warp_subscriptions",
19-
# TODO enable async tests
20-
"juniper_rocket_async",
2121
]

juniper/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,15 @@ See [#419](https://github.com/graphql-rust/juniper/pull/419).
1818
- `SchemaType` is now public
1919
- This is helpful when using `context.getSchema()` inside of your field resolvers
2020

21+
- Support subscriptions in GraphiQL
22+
2123
See [#569](https://github.com/graphql-rust/juniper/pull/569).
2224

2325
## Breaking Changes
2426

27+
- `juniper::graphiql` has moved to `juniper::http::graphiql`
28+
- `juniper::http::graphiql::graphiql_source` now requies a second parameter for subscriptions
29+
2530
- remove old `graphql_object!` macro, rename `object` proc macro to `graphql_object`
2631

2732
- Remove deprecated `ScalarValue` custom derive (renamed to GraphQLScalarValue)

juniper/release.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,15 @@ pre-release-replacements = [
2222
# Rocket
2323
{file="../juniper_rocket/Cargo.toml", search="juniper = \\{ version = \"[^\"]+\"", replace="juniper = { version = \"{{version}}\""},
2424
{file="../juniper_rocket/Cargo.toml", search="\\[dev-dependencies\\.juniper\\]\nversion = \"[^\"]+\"", replace="[dev-dependencies.juniper]\nversion = \"{{version}}\""},
25+
# Rocket Async
26+
{file="../juniper_rocket_async/Cargo.toml", search="juniper = \\{ version = \"[^\"]+\"", replace="juniper = { version = \"{{version}}\""},
27+
{file="../juniper_rocket_async/Cargo.toml", search="\\[dev-dependencies\\.juniper\\]\nversion = \"[^\"]+\"", replace="[dev-dependencies.juniper]\nversion = \"{{version}}\""},
2528
# Warp
2629
{file="../juniper_warp/Cargo.toml", search="juniper = \\{ version = \"[^\"]+\"", replace="juniper = { version = \"{{version}}\""},
2730
{file="../juniper_warp/Cargo.toml", search="\\[dev-dependencies\\.juniper\\]\nversion = \"[^\"]+\"", replace="[dev-dependencies.juniper]\nversion = \"{{version}}\""},
2831
# Subscriptions
2932
{file="../juniper_subscriptions/Cargo.toml", search="juniper = \\{ version = \"[^\"]+\"", replace="juniper = { version = \"{{version}}\""},
33+
# Actix-Web
34+
{file="../juniper_actix/Cargo.toml", search="juniper = \\{ version = \"[^\"]+\"", replace="juniper = { version = \"{{version}}\""},
35+
{file="../juniper_actix/Cargo.toml", search="\\[dev-dependencies\\.juniper\\]\nversion = \"[^\"]+\"", replace="[dev-dependencies.juniper]\nversion = \"{{version}}\""},
3036
]

juniper/src/http/graphiql.rs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
//! Utility module to generate a GraphiQL interface
22
33
/// Generate the HTML source to show a GraphiQL interface
4-
pub fn graphiql_source(graphql_endpoint_url: &str) -> String {
4+
///
5+
/// The subscriptions endpoint URL can optionally be provided. For example:
6+
///
7+
/// ```
8+
/// # use juniper::http::graphiql::graphiql_source;
9+
/// let graphiql = graphiql_source("/graphql", Some("ws://localhost:8080/subscriptions"));
10+
/// ```
11+
pub fn graphiql_source(
12+
graphql_endpoint_url: &str,
13+
subscriptions_endpoint_url: Option<&str>,
14+
) -> String {
15+
let subscriptions_endpoint = if let Some(sub_url) = subscriptions_endpoint_url {
16+
sub_url
17+
} else {
18+
""
19+
};
20+
521
let stylesheet_source = r#"
622
<style>
723
html, body, #app {
@@ -14,6 +30,10 @@ pub fn graphiql_source(graphql_endpoint_url: &str) -> String {
1430
"#;
1531
let fetcher_source = r#"
1632
<script>
33+
if (usingSubscriptions) {
34+
var subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient(GRAPHQL_SUBSCRIPTIONS_URL, { reconnect: true });
35+
}
36+
1737
function graphQLFetcher(params) {
1838
return fetch(GRAPHQL_URL, {
1939
method: 'post',
@@ -33,9 +53,12 @@ pub fn graphiql_source(graphql_endpoint_url: &str) -> String {
3353
}
3454
});
3555
}
56+
57+
var fetcher = usingSubscriptions ? window.GraphiQLSubscriptionsFetcher.graphQLFetcher(subscriptionsClient, graphQLFetcher) : graphQLFetcher;
58+
3659
ReactDOM.render(
3760
React.createElement(GraphiQL, {
38-
fetcher: graphQLFetcher,
61+
fetcher,
3962
}),
4063
document.querySelector('#app'));
4164
</script>
@@ -53,16 +76,22 @@ pub fn graphiql_source(graphql_endpoint_url: &str) -> String {
5376
<body>
5477
<div id="app"></div>
5578
<script src="//cdnjs.cloudflare.com/ajax/libs/fetch/2.0.3/fetch.js"></script>
79+
<script src="//unpkg.com/[email protected]/browser/client.js"></script>
80+
<script src="//unpkg.com/[email protected]/browser/client.js"></script>
5681
<script src="//cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
5782
<script src="//cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
5883
<script src="//cdn.jsdelivr.net/npm/[email protected]/graphiql.min.js"></script>
5984
<script>var GRAPHQL_URL = '{graphql_url}';</script>
85+
<script>var usingSubscriptions = {using_subscriptions};</script>
86+
<script>var GRAPHQL_SUBSCRIPTIONS_URL = '{graphql_subscriptions_url}';</script>
6087
{fetcher_source}
6188
</body>
6289
</html>
6390
"#,
6491
graphql_url = graphql_endpoint_url,
6592
stylesheet_source = stylesheet_source,
66-
fetcher_source = fetcher_source
93+
fetcher_source = fetcher_source,
94+
graphql_subscriptions_url = subscriptions_endpoint,
95+
using_subscriptions = subscriptions_endpoint_url.is_some(),
6796
)
6897
}

juniper/src/http/mod.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,128 @@ where
217217
}
218218
}
219219

220+
/// Simple wrapper around GraphQLRequest to allow the handling of Batch requests
221+
#[derive(Debug, serde_derive::Deserialize, PartialEq)]
222+
#[serde(untagged)]
223+
#[serde(bound = "InputValue<S>: Deserialize<'de>")]
224+
pub enum GraphQLBatchRequest<S = DefaultScalarValue>
225+
where
226+
S: ScalarValue,
227+
{
228+
/// A single operation request.
229+
Single(GraphQLRequest<S>),
230+
/// A batch operation request.
231+
Batch(Vec<GraphQLRequest<S>>),
232+
}
233+
234+
impl<S> GraphQLBatchRequest<S>
235+
where
236+
S: ScalarValue,
237+
{
238+
/// Execute a GraphQL batch request synchronously using the specified schema and context
239+
///
240+
/// This is a simple wrapper around the `execute_sync` function exposed in GraphQLRequest.
241+
pub fn execute_sync<'a, CtxT, QueryT, MutationT, SubscriptionT>(
242+
&'a self,
243+
root_node: &'a crate::RootNode<QueryT, MutationT, SubscriptionT, S>,
244+
context: &CtxT,
245+
) -> GraphQLBatchResponse<'a, S>
246+
where
247+
QueryT: crate::GraphQLType<S, Context = CtxT>,
248+
MutationT: crate::GraphQLType<S, Context = CtxT>,
249+
SubscriptionT: crate::GraphQLType<S, Context = CtxT>,
250+
{
251+
match *self {
252+
GraphQLBatchRequest::Single(ref request) => {
253+
GraphQLBatchResponse::Single(request.execute_sync(root_node, context))
254+
}
255+
GraphQLBatchRequest::Batch(ref requests) => GraphQLBatchResponse::Batch(
256+
requests
257+
.iter()
258+
.map(|request| request.execute_sync(root_node, context))
259+
.collect(),
260+
),
261+
}
262+
}
263+
264+
/// Executes a GraphQL request using the specified schema and context
265+
///
266+
/// This is a simple wrapper around the `execute` function exposed in
267+
/// GraphQLRequest
268+
pub async fn execute<'a, CtxT, QueryT, MutationT, SubscriptionT>(
269+
&'a self,
270+
root_node: &'a crate::RootNode<'a, QueryT, MutationT, SubscriptionT, S>,
271+
context: &'a CtxT,
272+
) -> GraphQLBatchResponse<'a, S>
273+
where
274+
QueryT: crate::GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
275+
QueryT::TypeInfo: Send + Sync,
276+
MutationT: crate::GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
277+
MutationT::TypeInfo: Send + Sync,
278+
SubscriptionT: crate::GraphQLSubscriptionType<S, Context = CtxT> + Send + Sync,
279+
SubscriptionT::TypeInfo: Send + Sync,
280+
CtxT: Send + Sync,
281+
S: Send + Sync,
282+
{
283+
match *self {
284+
GraphQLBatchRequest::Single(ref request) => {
285+
let res = request.execute(root_node, context).await;
286+
GraphQLBatchResponse::Single(res)
287+
}
288+
GraphQLBatchRequest::Batch(ref requests) => {
289+
let futures = requests
290+
.iter()
291+
.map(|request| request.execute(root_node, context))
292+
.collect::<Vec<_>>();
293+
let responses = futures::future::join_all(futures).await;
294+
295+
GraphQLBatchResponse::Batch(responses)
296+
}
297+
}
298+
}
299+
300+
/// The operation names of the request.
301+
pub fn operation_names(&self) -> Vec<Option<&str>> {
302+
match self {
303+
GraphQLBatchRequest::Single(req) => vec![req.operation_name()],
304+
GraphQLBatchRequest::Batch(reqs) => {
305+
reqs.iter().map(|req| req.operation_name()).collect()
306+
}
307+
}
308+
}
309+
}
310+
311+
/// Simple wrapper around the result (GraphQLResponse) from executing a GraphQLBatchRequest
312+
///
313+
/// This struct implements Serialize, so you can simply serialize this
314+
/// to JSON and send it over the wire. use the `is_ok` to determine
315+
/// wheter to send a 200 or 400 HTTP status code.
316+
#[derive(serde_derive::Serialize)]
317+
#[serde(untagged)]
318+
pub enum GraphQLBatchResponse<'a, S = DefaultScalarValue>
319+
where
320+
S: ScalarValue,
321+
{
322+
/// Result of a single operation in a GraphQL request.
323+
Single(GraphQLResponse<'a, S>),
324+
/// Result of a batch operation in a GraphQL request.
325+
Batch(Vec<GraphQLResponse<'a, S>>),
326+
}
327+
328+
impl<'a, S> GraphQLBatchResponse<'a, S>
329+
where
330+
S: ScalarValue,
331+
{
332+
/// Returns if all the GraphQLResponse in this operation are ok,
333+
/// you can use it to determine wheter to send a 200 or 400 HTTP status code.
334+
pub fn is_ok(&self) -> bool {
335+
match self {
336+
GraphQLBatchResponse::Single(res) => res.is_ok(),
337+
GraphQLBatchResponse::Batch(reses) => reses.iter().all(|res| res.is_ok()),
338+
}
339+
}
340+
}
341+
220342
#[cfg(any(test, feature = "expose-test-schema"))]
221343
#[allow(missing_docs)]
222344
pub mod tests {

juniper/src/lib.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,6 @@ mod validation;
142142
// https://github.com/rust-lang/cargo/issues/1520
143143
pub mod http;
144144
pub mod integrations;
145-
// TODO: remove this alias export in 0.10. (breaking change)
146-
pub use crate::http::graphiql;
147145

148146
#[cfg(all(test, not(feature = "expose-test-schema")))]
149147
mod tests;

juniper_actix/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/target
2+
/examples/**/target/**/*
3+
**/*.rs.bk
4+
Cargo.lock

juniper_actix/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# master
2+
3+
- Initial Release

juniper_actix/Cargo.toml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[package]
2+
name = "juniper_actix"
3+
version = "0.1.0"
4+
authors = ["Jordao Rosario <[email protected]>"]
5+
description = "Juniper GraphQL integration with Actix"
6+
license = "BSD-2-Clause"
7+
documentation = "https://docs.rs/juniper_actix"
8+
repository = "https://github.com/graphql-rust/juniper"
9+
edition = "2018"
10+
11+
12+
[dependencies]
13+
actix = "0.9.0"
14+
actix-rt = "1.0.0"
15+
actix-web = { version = "2.0.0", features = ["rustls"] }
16+
actix-web-actors = "2.0.0"
17+
futures = { version = "0.3.1", features = ["compat"] }
18+
juniper = { version = "0.14.2", path = "../juniper", default-features = false }
19+
tokio = { version = "0.2", features = ["time"] }
20+
serde_json = "1.0.24"
21+
serde_derive = "1.0.75"
22+
failure = "0.1.7"
23+
serde = "1.0.75"
24+
25+
[dev-dependencies]
26+
juniper = { version = "0.14.2", path = "../juniper", features = ["expose-test-schema", "serde_json"] }
27+
env_logger = "0.5.11"
28+
log = "0.4.3"
29+
percent-encoding = "1.0"
30+
tokio = { version = "0.2", features = ["rt-core", "macros", "blocking"] }
31+
actix-cors = "0.2.0"
32+
actix-identity = "0.2.0"
33+
bytes = "0.5.4"

juniper_actix/LICENSE

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
BSD 2-Clause License
2+
3+
Copyright (c) 2018, Jordao Rosario
4+
All rights reserved.
5+
6+
Redistribution and use in source and binary forms, with or without
7+
modification, are permitted provided that the following conditions are met:
8+
9+
* Redistributions of source code must retain the above copyright notice, this
10+
list of conditions and the following disclaimer.
11+
12+
* Redistributions in binary form must reproduce the above copyright notice,
13+
this list of conditions and the following disclaimer in the documentation
14+
and/or other materials provided with the distribution.
15+
16+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

0 commit comments

Comments
 (0)