Skip to content

Commit 2eae1c5

Browse files
committed
add juniper_graphql_transport_ws crate for new subscription protocol
1 parent 91064e9 commit 2eae1c5

File tree

15 files changed

+1759
-8
lines changed

15 files changed

+1759
-8
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ members = [
1212
"juniper_rocket",
1313
"juniper_subscriptions",
1414
"juniper_graphql_ws",
15+
"juniper_graphql_transport_ws",
1516
"juniper_warp",
1617
"juniper_actix",
1718
"tests/codegen",
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
`juniper_graphql_transport_ws` changelog
2+
==============================
3+
4+
All user visible changes to `juniper_graphql_transport_ws` crate will be documented in this file. This project uses [Semantic Versioning 2.0.0].
5+
6+
7+
8+
9+
## master
10+
11+
12+
13+
14+
[`juniper` crate]: https://docs.rs/juniper
15+
[`juniper_subscriptions` crate]: https://docs.rs/juniper_subscriptions
16+
[Semantic Versioning 2.0.0]: https://semver.org
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "juniper_graphql_transport_ws"
3+
version = "0.4.0-dev"
4+
edition = "2021"
5+
rust-version = "1.65"
6+
description = "GraphQL over WebSocket Protocol implementation for `juniper` crate."
7+
license = "BSD-2-Clause"
8+
authors = ["Christopher Brown <[email protected]>"]
9+
documentation = "https://docs.rs/juniper_graphql_transport_ws"
10+
homepage = "https://github.com/graphql-rust/juniper/tree/master/juniper_graphql_transport_ws"
11+
repository = "https://github.com/graphql-rust/juniper"
12+
readme = "README.md"
13+
categories = ["asynchronous", "web-programming", "web-programming::http-server"]
14+
keywords = ["apollo", "graphql", "graphql-ws", "subscription", "websocket"]
15+
exclude = ["/release.toml"]
16+
17+
[dependencies]
18+
juniper = { version = "0.16.0-dev", path = "../juniper", default-features = false }
19+
juniper_subscriptions = { version = "0.17.0-dev", path = "../juniper_subscriptions" }
20+
serde = { version = "1.0.122", features = ["derive"], default-features = false }
21+
tokio = { version = "1.0", features = ["macros", "rt", "time"], default-features = false }
22+
23+
[dev-dependencies]
24+
serde_json = "1.0.18"

juniper_graphql_transport_ws/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-2022, Christopher Brown
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.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
`juniper_graphql_transport_ws` crate
2+
==========================
3+
4+
[![Crates.io](https://img.shields.io/crates/v/juniper_graphql_transport_ws.svg?maxAge=2592000)](https://crates.io/crates/juniper_graphql_transport_ws)
5+
[![Documentation](https://docs.rs/juniper_graphql_transport_ws/badge.svg)](https://docs.rs/juniper_graphql_transport_ws)
6+
[![CI](https://github.com/graphql-rust/juniper/workflows/CI/badge.svg?branch=master "CI")](https://github.com/graphql-rust/juniper/actions?query=workflow%3ACI+branch%3Amaster)
7+
[![Rust 1.65+](https://img.shields.io/badge/rustc-1.65+-lightgray.svg "Rust 1.65+")](https://blog.rust-lang.org/2022/11/03/Rust-1.65.0.html)
8+
9+
- [Changelog](https://github.com/graphql-rust/juniper/blob/master/juniper_graphql_transport_ws/CHANGELOG.md)
10+
11+
This crate contains an implementation of the [graphql-transport-ws WebSocket subprotocol], as used by [Apollo].
12+
13+
14+
15+
16+
## License
17+
18+
This project is licensed under [BSD 2-Clause License](https://github.com/graphql-rust/juniper/blob/master/juniper_graphql_transport_ws/LICENSE).
19+
20+
21+
22+
23+
[Apollo]: https://www.apollographql.com
24+
[graphql-transport-ws WebSocket subprotocol]: https://github.com/enisdenjo/graphql-ws/blob/fbb763a662802a6a2584b0cbeb9cf1bde38158e0/PROTOCOL.md
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[[pre-release-replacements]]
2+
file = "../juniper_actix/Cargo.toml"
3+
exactly = 1
4+
search = "juniper_graphql_transport_ws = \\{ version = \"[^\"]+\""
5+
replace = "juniper_graphql_transport_ws = { version = \"{{version}}\""
6+
7+
[[pre-release-replacements]]
8+
file = "../juniper_warp/Cargo.toml"
9+
exactly = 1
10+
search = "juniper_graphql_transport_ws = \\{ version = \"[^\"]+\""
11+
replace = "juniper_graphql_transport_ws = { version = \"{{version}}\""
12+
13+
[[pre-release-replacements]]
14+
file = "CHANGELOG.md"
15+
max = 1
16+
min = 0
17+
search = "## master"
18+
replace = "## [{{version}}] · {{date}}\n[{{version}}]: /../../tree/{{crate_name}}-v{{version}}/{{crate_name}}"
19+
20+
[[pre-release-replacements]]
21+
file = "README.md"
22+
exactly = 2
23+
search = "graphql-rust/juniper/blob/[^/]+/"
24+
replace = "graphql-rust/juniper/blob/{{crate_name}}-v{{version}}/"
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
use juniper::Variables;
2+
use serde::Deserialize;
3+
4+
use crate::utils::default_for_null;
5+
6+
/// The payload for a client's "start" message. This triggers execution of a query, mutation, or
7+
/// subscription.
8+
#[derive(Debug, Deserialize, PartialEq)]
9+
#[serde(bound(deserialize = "S: Deserialize<'de>"))]
10+
#[serde(rename_all = "camelCase")]
11+
pub struct SubscribePayload<S> {
12+
/// The document body.
13+
pub query: String,
14+
15+
/// The optional variables.
16+
#[serde(default, deserialize_with = "default_for_null")]
17+
pub variables: Variables<S>,
18+
19+
/// The optional operation name (required if the document contains multiple operations).
20+
pub operation_name: Option<String>,
21+
22+
/// The optional extension data.
23+
#[serde(default, deserialize_with = "default_for_null")]
24+
pub extensions: Variables<S>,
25+
}
26+
27+
/// ClientMessage defines the message types that clients can send.
28+
#[derive(Debug, Deserialize, PartialEq)]
29+
#[serde(bound(deserialize = "S: Deserialize<'de>"))]
30+
#[serde(rename_all = "snake_case")]
31+
#[serde(tag = "type")]
32+
pub enum ClientMessage<S> {
33+
/// ConnectionInit is sent by the client upon connecting.
34+
ConnectionInit {
35+
/// Optional parameters of any type sent from the client. These are often used for
36+
/// authentication.
37+
#[serde(default, deserialize_with = "default_for_null")]
38+
payload: Variables<S>,
39+
},
40+
/// Ping is used for detecting failed connections, displaying latency metrics or other types of network probing.
41+
Ping {
42+
/// Optional parameters of any type used to transfer additional details about the ping.
43+
#[serde(default, deserialize_with = "default_for_null")]
44+
payload: Variables<S>,
45+
},
46+
/// The response to the `Ping` message.
47+
Pong {
48+
/// Optional parameters of any type used to transfer additional details about the pong.
49+
#[serde(default, deserialize_with = "default_for_null")]
50+
payload: Variables<S>,
51+
},
52+
/// Requests an operation specified in the message payload.
53+
Subscribe {
54+
/// The id of the operation. This can be anything, but must be unique. If there are other
55+
/// in-flight operations with the same id, the message will cause an error.
56+
id: String,
57+
58+
/// The query, variables, and operation name.
59+
payload: SubscribePayload<S>,
60+
},
61+
/// Indicates that the client has stopped listening and wants to complete the subscription.
62+
Complete {
63+
/// The id of the operation to stop.
64+
id: String,
65+
},
66+
}
67+
68+
#[cfg(test)]
69+
mod test {
70+
use juniper::{graphql_vars, DefaultScalarValue};
71+
72+
use super::*;
73+
74+
#[test]
75+
fn test_deserialization() {
76+
type ClientMessage = super::ClientMessage<DefaultScalarValue>;
77+
78+
assert_eq!(
79+
ClientMessage::ConnectionInit {
80+
payload: graphql_vars! {"foo": "bar"},
81+
},
82+
serde_json::from_str(r##"{"type": "connection_init", "payload": {"foo": "bar"}}"##)
83+
.unwrap(),
84+
);
85+
86+
assert_eq!(
87+
ClientMessage::ConnectionInit {
88+
payload: graphql_vars! {},
89+
},
90+
serde_json::from_str(r##"{"type": "connection_init"}"##).unwrap(),
91+
);
92+
93+
assert_eq!(
94+
ClientMessage::Subscribe {
95+
id: "foo".into(),
96+
payload: SubscribePayload {
97+
query: "query MyQuery { __typename }".into(),
98+
variables: graphql_vars! {"foo": "bar"},
99+
operation_name: Some("MyQuery".into()),
100+
extensions: Default::default(),
101+
},
102+
},
103+
serde_json::from_str(
104+
r##"{"type": "subscribe", "id": "foo", "payload": {
105+
"query": "query MyQuery { __typename }",
106+
"variables": {
107+
"foo": "bar"
108+
},
109+
"operationName": "MyQuery"
110+
}}"##
111+
)
112+
.unwrap(),
113+
);
114+
115+
assert_eq!(
116+
ClientMessage::Subscribe {
117+
id: "foo".into(),
118+
payload: SubscribePayload {
119+
query: "query MyQuery { __typename }".into(),
120+
variables: graphql_vars! {},
121+
operation_name: None,
122+
extensions: Default::default(),
123+
},
124+
},
125+
serde_json::from_str(
126+
r##"{"type": "subscribe", "id": "foo", "payload": {
127+
"query": "query MyQuery { __typename }"
128+
}}"##
129+
)
130+
.unwrap(),
131+
);
132+
133+
assert_eq!(
134+
ClientMessage::Complete { id: "foo".into() },
135+
serde_json::from_str(r##"{"type": "complete", "id": "foo"}"##).unwrap(),
136+
);
137+
}
138+
139+
#[test]
140+
fn test_deserialization_of_null() -> serde_json::Result<()> {
141+
let payload = r#"{"query":"query","variables":null}"#;
142+
let payload: SubscribePayload<DefaultScalarValue> = serde_json::from_str(payload)?;
143+
144+
let expected = SubscribePayload {
145+
query: "query".into(),
146+
variables: graphql_vars! {},
147+
operation_name: None,
148+
extensions: Default::default(),
149+
};
150+
151+
assert_eq!(expected, payload);
152+
153+
Ok(())
154+
}
155+
}

0 commit comments

Comments
 (0)