Skip to content

Add juniper_graphql_transport_ws crate for new subscription protocol #1158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ members = [
"juniper_rocket",
"juniper_subscriptions",
"juniper_graphql_ws",
"juniper_graphql_transport_ws",
"juniper_warp",
"juniper_actix",
"tests/codegen",
Expand Down
16 changes: 16 additions & 0 deletions juniper_graphql_transport_ws/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
`juniper_graphql_transport_ws` changelog
==============================

All user visible changes to `juniper_graphql_transport_ws` crate will be documented in this file. This project uses [Semantic Versioning 2.0.0].




## master




[`juniper` crate]: https://docs.rs/juniper
[`juniper_subscriptions` crate]: https://docs.rs/juniper_subscriptions
[Semantic Versioning 2.0.0]: https://semver.org
24 changes: 24 additions & 0 deletions juniper_graphql_transport_ws/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "juniper_graphql_transport_ws"
version = "0.4.0-dev"
edition = "2021"
rust-version = "1.65"
description = "GraphQL over WebSocket Protocol implementation for `juniper` crate."
license = "BSD-2-Clause"
authors = ["Christopher Brown <ccbrown112@gmail.com>"]
documentation = "https://docs.rs/juniper_graphql_transport_ws"
homepage = "https://github.com/graphql-rust/juniper/tree/master/juniper_graphql_transport_ws"
repository = "https://github.com/graphql-rust/juniper"
readme = "README.md"
categories = ["asynchronous", "web-programming", "web-programming::http-server"]
keywords = ["apollo", "graphql", "graphql-ws", "subscription", "websocket"]
exclude = ["/release.toml"]

[dependencies]
juniper = { version = "0.16.0-dev", path = "../juniper", default-features = false }
juniper_subscriptions = { version = "0.17.0-dev", path = "../juniper_subscriptions" }
serde = { version = "1.0.122", features = ["derive"], default-features = false }
tokio = { version = "1.0", features = ["macros", "rt", "time"], default-features = false }

[dev-dependencies]
serde_json = "1.0.18"
25 changes: 25 additions & 0 deletions juniper_graphql_transport_ws/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
BSD 2-Clause License

Copyright (c) 2018-2022, Christopher Brown
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 changes: 24 additions & 0 deletions juniper_graphql_transport_ws/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
`juniper_graphql_transport_ws` crate
==========================

[![Crates.io](https://img.shields.io/crates/v/juniper_graphql_transport_ws.svg?maxAge=2592000)](https://crates.io/crates/juniper_graphql_transport_ws)
[![Documentation](https://docs.rs/juniper_graphql_transport_ws/badge.svg)](https://docs.rs/juniper_graphql_transport_ws)
[![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)
[![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)

- [Changelog](https://github.com/graphql-rust/juniper/blob/master/juniper_graphql_transport_ws/CHANGELOG.md)

This crate contains an implementation of the [graphql-transport-ws WebSocket subprotocol], as used by [Apollo].




## License

This project is licensed under [BSD 2-Clause License](https://github.com/graphql-rust/juniper/blob/master/juniper_graphql_transport_ws/LICENSE).




[Apollo]: https://www.apollographql.com
[graphql-transport-ws WebSocket subprotocol]: https://github.com/enisdenjo/graphql-ws/blob/fbb763a662802a6a2584b0cbeb9cf1bde38158e0/PROTOCOL.md
24 changes: 24 additions & 0 deletions juniper_graphql_transport_ws/release.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[[pre-release-replacements]]
file = "../juniper_actix/Cargo.toml"
exactly = 1
search = "juniper_graphql_transport_ws = \\{ version = \"[^\"]+\""
replace = "juniper_graphql_transport_ws = { version = \"{{version}}\""

[[pre-release-replacements]]
file = "../juniper_warp/Cargo.toml"
exactly = 1
search = "juniper_graphql_transport_ws = \\{ version = \"[^\"]+\""
replace = "juniper_graphql_transport_ws = { version = \"{{version}}\""

[[pre-release-replacements]]
file = "CHANGELOG.md"
max = 1
min = 0
search = "## master"
replace = "## [{{version}}] · {{date}}\n[{{version}}]: /../../tree/{{crate_name}}-v{{version}}/{{crate_name}}"

[[pre-release-replacements]]
file = "README.md"
exactly = 2
search = "graphql-rust/juniper/blob/[^/]+/"
replace = "graphql-rust/juniper/blob/{{crate_name}}-v{{version}}/"
155 changes: 155 additions & 0 deletions juniper_graphql_transport_ws/src/client_message.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
use juniper::Variables;
use serde::Deserialize;

use crate::utils::default_for_null;

/// The payload for a client's "start" message. This triggers execution of a query, mutation, or
/// subscription.
#[derive(Debug, Deserialize, PartialEq)]
#[serde(bound(deserialize = "S: Deserialize<'de>"))]
#[serde(rename_all = "camelCase")]
pub struct SubscribePayload<S> {
/// The document body.
pub query: String,

/// The optional variables.
#[serde(default, deserialize_with = "default_for_null")]
pub variables: Variables<S>,

/// The optional operation name (required if the document contains multiple operations).
pub operation_name: Option<String>,

/// The optional extension data.
#[serde(default, deserialize_with = "default_for_null")]
pub extensions: Variables<S>,
}

/// ClientMessage defines the message types that clients can send.
#[derive(Debug, Deserialize, PartialEq)]
#[serde(bound(deserialize = "S: Deserialize<'de>"))]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type")]
pub enum ClientMessage<S> {
/// ConnectionInit is sent by the client upon connecting.
ConnectionInit {
/// Optional parameters of any type sent from the client. These are often used for
/// authentication.
#[serde(default, deserialize_with = "default_for_null")]
payload: Variables<S>,
},
/// Ping is used for detecting failed connections, displaying latency metrics or other types of network probing.
Ping {
/// Optional parameters of any type used to transfer additional details about the ping.
#[serde(default, deserialize_with = "default_for_null")]
payload: Variables<S>,
},
/// The response to the `Ping` message.
Pong {
/// Optional parameters of any type used to transfer additional details about the pong.
#[serde(default, deserialize_with = "default_for_null")]
payload: Variables<S>,
},
/// Requests an operation specified in the message payload.
Subscribe {
/// The id of the operation. This can be anything, but must be unique. If there are other
/// in-flight operations with the same id, the message will cause an error.
id: String,

/// The query, variables, and operation name.
payload: SubscribePayload<S>,
},
/// Indicates that the client has stopped listening and wants to complete the subscription.
Complete {
/// The id of the operation to stop.
id: String,
},
}

#[cfg(test)]
mod test {
use juniper::{graphql_vars, DefaultScalarValue};

use super::*;

#[test]
fn test_deserialization() {
type ClientMessage = super::ClientMessage<DefaultScalarValue>;

assert_eq!(
ClientMessage::ConnectionInit {
payload: graphql_vars! {"foo": "bar"},
},
serde_json::from_str(r##"{"type": "connection_init", "payload": {"foo": "bar"}}"##)
.unwrap(),
);

assert_eq!(
ClientMessage::ConnectionInit {
payload: graphql_vars! {},
},
serde_json::from_str(r##"{"type": "connection_init"}"##).unwrap(),
);

assert_eq!(
ClientMessage::Subscribe {
id: "foo".into(),
payload: SubscribePayload {
query: "query MyQuery { __typename }".into(),
variables: graphql_vars! {"foo": "bar"},
operation_name: Some("MyQuery".into()),
extensions: Default::default(),
},
},
serde_json::from_str(
r##"{"type": "subscribe", "id": "foo", "payload": {
"query": "query MyQuery { __typename }",
"variables": {
"foo": "bar"
},
"operationName": "MyQuery"
}}"##
)
.unwrap(),
);

assert_eq!(
ClientMessage::Subscribe {
id: "foo".into(),
payload: SubscribePayload {
query: "query MyQuery { __typename }".into(),
variables: graphql_vars! {},
operation_name: None,
extensions: Default::default(),
},
},
serde_json::from_str(
r##"{"type": "subscribe", "id": "foo", "payload": {
"query": "query MyQuery { __typename }"
}}"##
)
.unwrap(),
);

assert_eq!(
ClientMessage::Complete { id: "foo".into() },
serde_json::from_str(r##"{"type": "complete", "id": "foo"}"##).unwrap(),
);
}

#[test]
fn test_deserialization_of_null() -> serde_json::Result<()> {
let payload = r#"{"query":"query","variables":null}"#;
let payload: SubscribePayload<DefaultScalarValue> = serde_json::from_str(payload)?;

let expected = SubscribePayload {
query: "query".into(),
variables: graphql_vars! {},
operation_name: None,
extensions: Default::default(),
};

assert_eq!(expected, payload);

Ok(())
}
}
Loading