diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d934cc7..190eb7b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,8 +18,8 @@ jobs: uses: actions/create-github-app-token@v1 id: generate-token with: - app-id: ${{ secrets.RELEASE_APP_ID }} - private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + app-id: ${{ secrets.RELEASE_APP_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - name: Checkout repository uses: actions/checkout@v4 with: diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index c386a14..e659ba5 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -16,57 +16,44 @@ env: jobs: check-format: # Skip draft release PRs - if: ${{ github.actor_id != '166155226' || github.event_name != 'pull_request' || github.event.pull_request.draft == false }} + if: github.actor_id != '166155226' || github.event_name != 'pull_request' || github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: dtolnay/rust-toolchain@1.76.0 with: components: rustfmt - - name: Check formatting - uses: actions-rs/cargo@v1 - with: - command: fmt - args: -- --check + - name: Check format + run: cargo fmt --all --check build: # Skip draft release PRs - if: ${{ github.actor_id != '166155226' || github.event_name != 'pull_request' || github.event.pull_request.draft == false }} + if: github.actor_id != '166155226' || github.event_name != 'pull_request' || github.event.pull_request.draft == false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: dtolnay/rust-toolchain@1.76.0 + with: + components: clippy - uses: Swatinem/rust-cache@v2 - name: Build - shell: bash - run: | - cargo build --all-features - + run: cargo build --workspace --all-features - name: Build tests - shell: bash - run: | - cargo test --all-features --no-run - + run: cargo test --workspace --all-features --no-run - name: Run tests - shell: bash - run: | - cargo test --all-features - + run: cargo test --workspace --all-features - name: Build examples - shell: bash - run: | - cargo build --all --examples - - - name: Build example tests - shell: bash - run: | - cargo test --all --examples --no-run - cargo test --all --examples - - - name: Run example tests - shell: bash - run: | - cargo test --all --examples + run: cargo build --workspace --all-features --examples + - name: Build examples tests + run: cargo test --workspace --all-features --examples --no-run + - name: Run examples tests + run: cargo test --workspace --all-features --examples + - name: Run clippy + run: cargo clippy --all --all-features diff --git a/.gitignore b/.gitignore index ea8c4bf..6301462 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,24 @@ -/target +# Generated by Cargo will have compiled files and executables +/debug/ +/target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Log +*.log + +# JetBrains +/.idea/ + +# Visual Studio Code +/.vscode/ + +# Local History for Visual Studio Code +/.history/ + +# Converage +/coverage/ diff --git a/CHANGELOG.md b/CHANGELOG.md index a3f868f..6a717ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,9 +22,9 @@ all APIs might be changed. ### Breaking Changes -- All Connection trait functions now return impl Future instead of BoxFuture +- All Connection trait functions now return impl Future instead of BoxFuture ([#108](https://github.com/obmarg/graphql-ws-client/pull/108)) -- Removed the legacy API that was deprecated in v0.8.0 +- Removed the legacy API that was deprecated in v0.8.0 ([#81](https://github.com/obmarg/graphql-ws-client/pull/81)) - The deprecated `async-tungstenite` feature has been removed. Use the `tungstenite` feature instead, which works with `async-tungtenite`, diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index c4eff8d..50a7cf9 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,21 +14,21 @@ appearance, race, religion, or sexual identity and orientation. Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or +- The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3cc23bd..2ff89a6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ There's a few different ways to contribute: please [create an issue][NewIssue] to discuss or send a message [on discord][Discord]. - If you'd like to contribute to an existing issue feel free to comment on the - issue and let us know. If anything isn't clear someone will be happy to + issue and let us know. If anything isn't clear someone will be happy to explain (the project is still fairly new, and I have treated the issue tracker like a notepad occasionally, sorry about that 😬). - If you'd like to contribute but you're not sure how: @@ -49,9 +49,10 @@ You can do this by: understand the context around & reason for the changes. 5. A maintainer will review the PR as soon as possible, and once it is approved will merge and make a release. - + [COC]: ./CODE_OF_CONDUCT.md [Discord]: https://discord.gg/Y5xDmDP [Discussions]: https://github.com/obmarg/graphql-ws-client/discussions/new [GFI]: https://github.com/obmarg/graphql-ws-client/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22 [Milestones]: https://github.com/obmarg/graphql-ws-client/milestones +[NewIssue]: https://github.com/obmarg/graphql-ws-client/issues/new/choose diff --git a/Cargo.toml b/Cargo.toml index 9c3c8ee..12ad439 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,13 @@ edition = "2021" resolver = "2" description = "A graphql over websockets client" keywords = ["graphql", "client", "api", "websockets", "subscriptions"] +categories = [ + "asynchronous", + "network-programming", + "wasm", + "web-programming", + "web-programming::websocket", +] license = "Apache-2.0" autoexamples = false documentation = "https://docs.rs/graphql-ws-client" @@ -58,8 +65,22 @@ tokio-stream = { version = "0.1", features = ["sync"] } graphql-ws-client.path = "." -graphql-ws-client.features = ["client-cynic", "client-graphql-client", "tungstenite"] +graphql-ws-client.features = [ + "client-cynic", + "client-graphql-client", + "tungstenite", +] [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] + +[workspace.lints.rust] +unsafe_code = "forbid" + +[workspace.lints.clippy] +cargo = { level = "warn", priority = -1 } +multiple_crate_versions = "allow" + +[lints] +workspace = true diff --git a/README.md b/README.md index 1a9bb83..01c5fa7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@
-

Graphql Websocket Client

+

GraphQL Websocket Client

Runtime agnostic graphql websocket client @@ -12,9 +12,9 @@

- Examples + Examples | - Changelog + Changelog

@@ -41,8 +41,8 @@ The library offers integrations with some popular GraphQL clients with feature f The documentation is quite limited at the moment, here are some sources: -1. The provided [examples](https://github.com/obmarg/graphql-ws-client/tree/master/examples/examples) -2. The reference documentation on [docs.rs](https://docs.rs/graphql-ws-client/) +1. The provided [examples](https://github.com/obmarg/graphql-ws-client/tree/main/examples/examples) +2. The reference documentation on [docs.rs](https://docs.rs/graphql-ws-client) ## Logging diff --git a/examples-wasm/Cargo.toml b/examples-wasm/Cargo.toml index d5e1d28..098755e 100644 --- a/examples-wasm/Cargo.toml +++ b/examples-wasm/Cargo.toml @@ -10,7 +10,7 @@ publish = false [dependencies] async-std = { version = "1.9", features = ["attributes"] } cynic = { version = "3" } -futures = { version = "0.3"} +futures = { version = "0.3" } log = "0.4" # wasm-specific @@ -27,3 +27,6 @@ features = ["cynic", "ws_stream_wasm"] [dev-dependencies] insta = "1.11" + +[lints] +workspace = true diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 30179be..5cf92bf 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -9,9 +9,12 @@ publish = false [dependencies] async-std = { version = "1.9", features = ["attributes"] } -async-tungstenite = { version = "0.26", features = ["async-std-runtime", "tokio-runtime"] } +async-tungstenite = { version = "0.26", features = [ + "async-std-runtime", + "tokio-runtime", +] } cynic = { version = "3" } -futures = { version = "0.3"} +futures = { version = "0.3" } graphql_client = { version = "0.14" } serde = "1" tokio = { version = "1.15", features = ["rt-multi-thread", "macros"] } @@ -22,6 +25,8 @@ version = "0.10.1" default-features = false features = ["cynic", "tungstenite"] - [dev-dependencies] insta = "1.11" + +[lints] +workspace = true diff --git a/examples/examples/cynic-mulitiple-subscriptions.rs b/examples/examples/cynic-mulitiple-subscriptions.rs index da06220..ad5f298 100644 --- a/examples/examples/cynic-mulitiple-subscriptions.rs +++ b/examples/examples/cynic-mulitiple-subscriptions.rs @@ -66,12 +66,12 @@ async fn main() { futures::join!( async move { while let Some(item) = first_subscription.next().await { - println!("{:?}", item); + println!("{item:?}"); } }, async move { while let Some(item) = second_subscription.next().await { - println!("{:?}", item); + println!("{item:?}"); } } ); diff --git a/examples/examples/cynic-single-subscription.rs b/examples/examples/cynic-single-subscription.rs index a0ba83c..24a77fe 100644 --- a/examples/examples/cynic-single-subscription.rs +++ b/examples/examples/cynic-single-subscription.rs @@ -58,7 +58,7 @@ async fn main() { .unwrap(); while let Some(item) = subscription.next().await { - println!("{:?}", item); + println!("{item:?}"); } } diff --git a/examples/examples/graphql-client-single-subscription.rs b/examples/examples/graphql-client-single-subscription.rs index b83ac01..1a1222f 100644 --- a/examples/examples/graphql-client-single-subscription.rs +++ b/examples/examples/graphql-client-single-subscription.rs @@ -40,6 +40,6 @@ async fn main() { .unwrap(); while let Some(item) = subscription.next().await { - println!("{:?}", item); + println!("{item:?}"); } } diff --git a/examples/examples/tokio.rs b/examples/examples/tokio.rs index e6e64b8..872c499 100644 --- a/examples/examples/tokio.rs +++ b/examples/examples/tokio.rs @@ -60,7 +60,7 @@ async fn main() { let mut stream = client.subscribe(build_query()).await.unwrap(); println!("Running subscription apparently?"); while let Some(item) = stream.next().await { - println!("{:?}", item); + println!("{item:?}"); } } diff --git a/src/doc_utils.rs b/src/doc_utils.rs index 540c5b0..aa00c37 100644 --- a/src/doc_utils.rs +++ b/src/doc_utils.rs @@ -27,4 +27,4 @@ impl crate::graphql::GraphqlOperation for Subscription { } } -pub fn spawn(_future: impl Future + Send + 'static) {} +pub fn spawn(_: impl Future + Send + 'static) {} diff --git a/src/graphql.rs b/src/graphql.rs index b8097d8..21fd6ee 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -23,7 +23,7 @@ pub trait GraphqlOperation: serde::Serialize { #[cfg(feature = "client-cynic")] mod cynic { - use super::*; + use super::GraphqlOperation; #[cfg_attr(docsrs, doc(cfg(feature = "client-cynic")))] impl GraphqlOperation @@ -47,11 +47,11 @@ pub use self::graphql_client::StreamingOperation; #[cfg(feature = "client-graphql-client")] mod graphql_client { - use super::*; + use super::GraphqlOperation; use ::graphql_client::{GraphQLQuery, QueryBody, Response}; use std::marker::PhantomData; - /// A streaming operation for a GraphQLQuery + /// A streaming operation for a [`GraphQLQuery`] #[cfg_attr(docsrs, doc(cfg(feature = "client-graphql-client")))] pub struct StreamingOperation { inner: QueryBody, @@ -59,7 +59,7 @@ mod graphql_client { } impl StreamingOperation { - /// Constructs a StreamingOperation + /// Constructs a [`StreamingOperation`] pub fn new(variables: Q::Variables) -> Self { Self { inner: Q::build_query(variables), diff --git a/src/lib.rs b/src/lib.rs index 6ac86b1..c79ae8b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,7 +56,9 @@ mod next; #[cfg(feature = "ws_stream_wasm")] #[cfg_attr(docsrs, doc(cfg(feature = "ws_stream_wasm")))] -/// Integration with the ws_stream_wasm library +/// Integration with the [ws_stream_wasm][1] library +/// +/// [1]: https://docs.rs/ws_stream/latest/ws_stream pub mod ws_stream_wasm; #[cfg(feature = "tungstenite")] diff --git a/src/next/actor.rs b/src/next/actor.rs index e98597f..0274752 100644 --- a/src/next/actor.rs +++ b/src/next/actor.rs @@ -50,7 +50,7 @@ impl ConnectionActor { async fn run(mut self) { while let Some(next) = self.next().await { let response = match next { - Next::Command(cmd) => self.handle_command(cmd).await, + Next::Command(cmd) => self.handle_command(cmd), Next::Message(message) => self.handle_message(message).await, }; @@ -75,7 +75,7 @@ impl ConnectionActor { .ok(); } - async fn handle_command(&mut self, cmd: ConnectionCommand) -> Option { + fn handle_command(&mut self, cmd: ConnectionCommand) -> Option { match cmd { ConnectionCommand::Subscribe { request, @@ -119,9 +119,8 @@ impl ConnectionActor { match event { event @ (Event::Next { .. } | Event::Error { .. }) => { - let id = match event.id().unwrap().parse::().ok() { - Some(id) => id, - None => return Some(Message::close(Reason::UnknownSubscription)), + let Some(id) = event.id().unwrap().parse::().ok() else { + return Some(Message::close(Reason::UnknownSubscription)); }; let sender = self.operations.entry(id); @@ -140,9 +139,8 @@ impl ConnectionActor { None } Event::Complete { id } => { - let id = match id.parse::().ok() { - Some(id) => id, - None => return Some(Message::close(Reason::UnknownSubscription)), + let Some(id) = id.parse::().ok() else { + return Some(Message::close(Reason::UnknownSubscription)); }; trace!("Stream complete"); @@ -169,7 +167,7 @@ impl ConnectionActor { let keep_alive = async { Select::KeepAlive(self.keep_alive_actor.next().await) }; match command.or(message).or(keep_alive).await { - Select::Command(Some(command)) => { + Select::Command(Some(command)) | Select::KeepAlive(Some(command)) => { return Some(Next::Command(command)); } Select::Command(None) => { @@ -180,11 +178,8 @@ impl ConnectionActor { self.keep_alive_actor = Box::pin(self.keep_alive.run()); return Some(Next::Message(message?)); } - Select::KeepAlive(Some(command)) => { - return Some(Next::Command(command)); - } Select::KeepAlive(None) => { - return self.keep_alive.report_timeout(); + return Some(self.keep_alive.report_timeout()); } } } @@ -260,14 +255,14 @@ impl Event { } impl KeepAliveSettings { - fn report_timeout(&self) -> Option { + fn report_timeout(&self) -> Next { warning!( "No messages received within keep-alive ({:?}s) from server. Closing the connection", self.interval.unwrap() ); - Some(Next::Command(ConnectionCommand::Close( + Next::Command(ConnectionCommand::Close( 4503, "Service unavailable. keep-alive failure".to_string(), - ))) + )) } } diff --git a/src/next/builder.rs b/src/next/builder.rs index c27b5b7..7105942 100644 --- a/src/next/builder.rs +++ b/src/next/builder.rs @@ -31,6 +31,7 @@ use super::{ /// # Ok(()) /// # } /// ``` +#[must_use] pub struct ClientBuilder { payload: Option, subscription_buffer_size: Option, @@ -39,7 +40,7 @@ pub struct ClientBuilder { } impl super::Client { - /// Creates a ClientBuilder with the given connection. + /// Creates a `ClientBuilder` with the given connection. /// /// ```rust /// use graphql_ws_client::Client; @@ -65,6 +66,10 @@ impl super::Client { impl ClientBuilder { /// Add payload to `connection_init` + /// + /// # Errors + /// + /// Will return `Err` if `payload` serialization fails. pub fn payload(self, payload: NewPayload) -> Result where NewPayload: Serialize, @@ -120,7 +125,7 @@ impl ClientBuilder { /// Note that this takes ownership of the client, so it cannot be /// used to run any more operations. /// - /// If users want to run mutliple operations on a connection they + /// If users want to run multiple operations on a connection they /// should use the `IntoFuture` impl to construct a `Client` pub async fn subscribe<'a, Operation>( self, @@ -176,7 +181,7 @@ impl ClientBuilder { reason.unwrap_or_default(), )) } - Some(Message::Ping) | Some(Message::Pong) => {} + Some(Message::Ping | Message::Pong) => {} Some(message @ Message::Text(_)) => { let event = message.deserialize::()?; match event { diff --git a/src/next/mod.rs b/src/next/mod.rs index f535927..df69a71 100644 --- a/src/next/mod.rs +++ b/src/next/mod.rs @@ -71,7 +71,7 @@ impl Client { } } - // Starts a streaming operation on this client. + /// Starts a streaming operation on this client. /// /// Returns a `Stream` of responses. pub async fn subscribe<'a, Operation>( @@ -115,7 +115,7 @@ impl Client { /// Gracefully closes the connection /// - /// This will stop all running subscriptions and shut down the ConnectionActor wherever + /// This will stop all running subscriptions and shut down the [`ConnectionActor`] wherever /// it is running. pub async fn close(self, code: u16, description: impl Into) { self.actor diff --git a/src/next/stream.rs b/src/next/stream.rs index 3188fed..28d2721 100644 --- a/src/next/stream.rs +++ b/src/next/stream.rs @@ -27,6 +27,10 @@ where Operation: GraphqlOperation + Send, { /// Stops the subscription by sending a Complete message to the server. + /// + /// # Errors + /// + /// Will return `Err` if the stop operation fails. pub async fn stop(self) -> Result<(), Error> { self.actor .send(ConnectionCommand::Cancel(self.id)) diff --git a/src/protocol.rs b/src/protocol.rs index 4b47f1d..8f7fe9d 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -69,9 +69,9 @@ pub enum Event { impl Event { pub fn id(&self) -> Option<&str> { match self { - Event::Next { id, .. } => Some(id.as_ref()), - Event::Complete { id, .. } => Some(id.as_ref()), - Event::Error { id, .. } => Some(id.as_ref()), + Event::Next { id, .. } | Event::Complete { id, .. } | Event::Error { id, .. } => { + Some(id.as_ref()) + } Event::Ping { .. } | Event::Pong { .. } | Event::ConnectionAck { .. } => None, } } diff --git a/src/sink_ext.rs b/src/sink_ext.rs index e2f7245..c2f26ee 100644 --- a/src/sink_ext.rs +++ b/src/sink_ext.rs @@ -7,7 +7,10 @@ use std::{ use futures_lite::ready; use futures_sink::Sink; -/// A very limited clone of futures::SinkExt to avoid having to pull the original in +/// A very limited clone of [futures::sink::SinkExt](1) to avoid having to +/// pull the original in +/// +/// [1]: https://docs.rs/futures/latest/futures/sink/trait.SinkExt.html pub trait SinkExt: Sink { fn send(&mut self, item: Item) -> Send<'_, Self, Item> where diff --git a/src/ws_stream_wasm.rs b/src/ws_stream_wasm.rs index d38ea36..863c5d8 100644 --- a/src/ws_stream_wasm.rs +++ b/src/ws_stream_wasm.rs @@ -4,7 +4,9 @@ use ws_stream_wasm::{WsEvent, WsMessage, WsMeta, WsStream}; use crate::{sink_ext::SinkExt, Error}; -/// A websocket connection for ws_stream_wasm +/// A websocket connection for [ws_stream_wasm][1] +/// +/// [1]: https://docs.rs/ws_stream/latest/ws_stream #[cfg_attr(docsrs, doc(cfg(feature = "ws_stream_wasm")))] pub struct Connection { messages: WsStream, @@ -13,7 +15,11 @@ pub struct Connection { } impl Connection { - /// Creates a new Connection from a WsMeta & WsTream combo + /// Creates a new Connection from a [`WsMeta`] and [`WsStream`] combo + /// + /// # Panics + /// + /// Will panic if `meta.observe` fails. pub async fn new((mut meta, messages): (WsMeta, WsStream)) -> Self { let event_stream = meta.observe(ObserveConfig::default()).await.unwrap(); @@ -42,9 +48,7 @@ impl crate::next::Connection for Connection { EventOrMessage::Event(WsEvent::Open | WsEvent::Closing) => { continue; } - EventOrMessage::Message(WsMessage::Text(text)) => return Some(Message::Text(text)), - EventOrMessage::Message(WsMessage::Binary(_)) => { // We shouldn't receive binary messages, but ignore them if we do continue; diff --git a/tests/subscription_server/mod.rs b/tests/subscription_server/mod.rs index d03fb4b..796b07d 100644 --- a/tests/subscription_server/mod.rs +++ b/tests/subscription_server/mod.rs @@ -116,6 +116,6 @@ pub struct SubscriptionRoot { impl SubscriptionRoot { async fn books(&self, _mutation_type: MutationType) -> impl Stream { println!("Subscription received"); - BroadcastStream::new(self.channel.subscribe()).filter_map(|r| r.ok()) + BroadcastStream::new(self.channel.subscribe()).filter_map(Result::ok) } }