From ea5e18d6aada52e88873cd1be4cd21d01ca3a471 Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Mon, 28 Aug 2023 21:32:42 +0300 Subject: [PATCH] Presence event engine (#164) feat(presence): add presence event engine feat(presence): add set state endpoint Add an endpoint which allows associating `user_id` state with a list of channels and groups. refactor(deserializer): refactor custom deserializer configuration Custom deserializer now can be set during client configuration. refactor(runtime): refactor runtime environment configuration Runtime environment now can be set during client configuration. refactor(parse): refactor PubNub endpoint response parsing --------- Co-authored-by: Xavrax --- Cargo.toml | 5 +- examples/subscribe.rs | 9 +- src/core/deserialize.rs | 12 + src/core/deserializer.rs | 24 +- .../subscribe => core/event_engine}/cancel.rs | 6 +- src/core/event_engine/effect_dispatcher.rs | 12 +- src/core/event_engine/mod.rs | 13 +- src/core/mod.rs | 12 +- src/core/runtime.rs | 57 +- ...{error_response.rs => service_response.rs} | 190 ++- src/core/transport.rs | 20 +- src/core/transport_request.rs | 153 +- src/core/utils/encoding.rs | 38 +- src/core/utils/mod.rs | 14 +- src/dx/access/builders/grant_token.rs | 154 +- src/dx/access/builders/mod.rs | 16 +- src/dx/access/builders/revoke.rs | 141 +- src/dx/access/mod.rs | 74 +- src/dx/access/payloads.rs | 5 +- src/dx/access/result.rs | 16 +- src/dx/mod.rs | 3 + src/dx/parse_token.rs | 15 +- src/dx/presence/builders/heartbeat.rs | 339 ++++ src/dx/presence/builders/leave.rs | 161 ++ src/dx/presence/builders/mod.rs | 38 + src/dx/presence/builders/set_state.rs | 199 +++ .../presence/event_engine/effect_handler.rs | 102 ++ .../event_engine/effects/heartbeat.rs | 185 +++ src/dx/presence/event_engine/effects/leave.rs | 105 ++ src/dx/presence/event_engine/effects/mod.rs | 249 +++ src/dx/presence/event_engine/effects/wait.rs | 52 + src/dx/presence/event_engine/event.rs | 107 ++ src/dx/presence/event_engine/invocation.rs | 108 ++ src/dx/presence/event_engine/mod.rs | 35 + src/dx/presence/event_engine/state.rs | 1374 +++++++++++++++++ src/dx/presence/event_engine/types.rs | 554 +++++++ src/dx/presence/mod.rs | 546 +++++++ src/dx/presence/presence_manager.rs | 125 ++ src/dx/presence/result.rs | 329 ++++ src/dx/publish/builders.rs | 165 +- src/dx/publish/mod.rs | 139 +- src/dx/publish/result.rs | 70 +- src/dx/pubnub_client.rs | 804 ++++++++-- src/dx/subscribe/builders/mod.rs | 24 +- src/dx/subscribe/builders/raw.rs | 137 +- src/dx/subscribe/builders/subscribe.rs | 274 ++-- src/dx/subscribe/builders/subscription.rs | 152 +- .../subscribe/event_engine/effect_handler.rs | 55 +- .../event_engine/effects/emit_messagess.rs | 54 - .../event_engine/effects/handshake.rs | 35 +- .../effects/handshake_reconnection.rs | 35 +- src/dx/subscribe/event_engine/effects/mod.rs | 179 +-- .../subscribe/event_engine/effects/receive.rs | 37 +- .../effects/receive_reconnection.rs | 79 +- src/dx/subscribe/event_engine/event.rs | 32 +- src/dx/subscribe/event_engine/invocation.rs | 59 +- src/dx/subscribe/event_engine/mod.rs | 62 +- src/dx/subscribe/event_engine/state.rs | 1083 +++++++------ src/dx/subscribe/event_engine/types.rs | 729 +++++++++ src/dx/subscribe/mod.rs | 377 +---- src/dx/subscribe/result.rs | 8 +- .../subscribe/subscription_configuration.rs | 63 - src/dx/subscribe/subscription_manager.rs | 158 +- src/lib.rs | 4 + src/providers/deserialization_serde.rs | 18 +- src/providers/futures_tokio.rs | 6 +- src/transport/middleware.rs | 12 +- src/transport/reqwest.rs | 157 +- 68 files changed, 7987 insertions(+), 2617 deletions(-) rename src/{dx/subscribe => core/event_engine}/cancel.rs (85%) rename src/core/{error_response.rs => service_response.rs} (87%) create mode 100644 src/dx/presence/builders/heartbeat.rs create mode 100644 src/dx/presence/builders/leave.rs create mode 100644 src/dx/presence/builders/mod.rs create mode 100644 src/dx/presence/builders/set_state.rs create mode 100644 src/dx/presence/event_engine/effect_handler.rs create mode 100644 src/dx/presence/event_engine/effects/heartbeat.rs create mode 100644 src/dx/presence/event_engine/effects/leave.rs create mode 100644 src/dx/presence/event_engine/effects/mod.rs create mode 100644 src/dx/presence/event_engine/effects/wait.rs create mode 100644 src/dx/presence/event_engine/event.rs create mode 100644 src/dx/presence/event_engine/invocation.rs create mode 100644 src/dx/presence/event_engine/mod.rs create mode 100644 src/dx/presence/event_engine/state.rs create mode 100644 src/dx/presence/event_engine/types.rs create mode 100644 src/dx/presence/mod.rs create mode 100644 src/dx/presence/presence_manager.rs create mode 100644 src/dx/presence/result.rs delete mode 100644 src/dx/subscribe/event_engine/effects/emit_messagess.rs create mode 100644 src/dx/subscribe/event_engine/types.rs delete mode 100644 src/dx/subscribe/subscription_configuration.rs diff --git a/Cargo.toml b/Cargo.toml index 874b30c2..2336e90d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,10 +14,10 @@ build = "build.rs" [features] # Enables all non-conflicting features -full = ["publish", "access", "serde", "reqwest", "aescbc", "parse_token", "blocking", "std", "tokio"] +full = ["publish", "subscribe", "presence", "access", "serde", "reqwest", "aescbc", "parse_token", "blocking", "std", "tokio"] # Enables all default features -default = ["publish", "subscribe", "serde", "reqwest", "aescbc", "std", "blocking", "tokio"] +default = ["publish", "subscribe", "presence", "serde", "reqwest", "aescbc", "std", "blocking", "tokio"] # [PubNub features] @@ -70,6 +70,7 @@ pubnub_only = ["aescbc", "parse_token", "blocking", "publish", "access", "subscr mock_getrandom = ["getrandom/custom"] # TODO: temporary treated as internal until we officially release it subscribe = ["dep:futures"] +presence = ["dep:futures"] [dependencies] async-trait = "0.1" diff --git a/examples/subscribe.rs b/examples/subscribe.rs index 04d3a3cf..63d540c5 100644 --- a/examples/subscribe.rs +++ b/examples/subscribe.rs @@ -32,7 +32,14 @@ async fn main() -> Result<(), Box> { let subscription = client .subscribe() - .channels(["my_channel".into(), "other_channel".into()].to_vec()) + .channels( + [ + "my_channel".into(), + "other_channel".into(), + "channel-test-history".into(), + ] + .to_vec(), + ) .heartbeat(10) .filter_expression("some_filter") .execute()?; diff --git a/src/core/deserialize.rs b/src/core/deserialize.rs index 3affb1c6..2e8bafff 100644 --- a/src/core/deserialize.rs +++ b/src/core/deserialize.rs @@ -24,3 +24,15 @@ pub trait Deserialize<'de>: Send + Sync { /// Deserialize the value fn deserialize(bytes: &'de [u8]) -> Result; } + +#[cfg(not(feature = "serde"))] +impl<'de, D> Deserialize<'de> for D +where + D: Sync + Send, +{ + type Type = D; + + fn deserialize(_bytes: &'de [u8]) -> Result { + unimplemented!("Please implement this method for type") + } +} diff --git a/src/core/deserializer.rs b/src/core/deserializer.rs index 2b3f29ef..a609383d 100644 --- a/src/core/deserializer.rs +++ b/src/core/deserializer.rs @@ -30,8 +30,8 @@ use crate::core::PubNubError; /// /// struct MyDeserializer; /// -/// impl Deserializer for MyDeserializer { -/// fn deserialize(&self, bytes: &[u8]) -> Result { +/// impl Deserializer for MyDeserializer { +/// fn deserialize(&self, bytes: &[u8]) -> Result { /// // ... /// # unimplemented!() /// } @@ -42,7 +42,7 @@ use crate::core::PubNubError; /// [`PublishResponseBody`]: ../../dx/publish/result/enum.PublishResponseBody.html /// [`GrantTokenResponseBody`]: ../../dx/access/result/enum.GrantTokenResponseBody.html /// [`RevokeTokenResponseBody`]: ../../dx/access/result/enum.RevokeTokenResponseBody.html -pub trait Deserializer: Send + Sync { +pub trait Deserializer: Send + Sync { /// Deserialize a `&Vec` into a `Result`. /// /// # Errors @@ -51,5 +51,21 @@ pub trait Deserializer: Send + Sync { /// deserialization fails. /// /// [`PubNubError::DeserializationError`]: ../enum.PubNubError.html#variant.DeserializationError - fn deserialize(&self, bytes: &[u8]) -> Result; + #[cfg(not(feature = "serde"))] + fn deserialize(&self, bytes: &[u8]) -> Result + where + T: for<'de> crate::core::Deserialize<'de>; + + /// Deserialize a `&Vec` into a `Result`. + /// + /// # Errors + /// + /// This method should return [`PubNubError::DeserializationError`] if the + /// deserialization fails. + /// + /// [`PubNubError::DeserializationError`]: ../enum.PubNubError.html#variant.DeserializationError + #[cfg(feature = "serde")] + fn deserialize(&self, bytes: &[u8]) -> Result + where + T: for<'de> serde::Deserialize<'de>; } diff --git a/src/dx/subscribe/cancel.rs b/src/core/event_engine/cancel.rs similarity index 85% rename from src/dx/subscribe/cancel.rs rename to src/core/event_engine/cancel.rs index c94d516e..2147d01c 100644 --- a/src/dx/subscribe/cancel.rs +++ b/src/core/event_engine/cancel.rs @@ -1,3 +1,7 @@ +//! Managed effects cancellation module. +//! +//! This module provides [`CancellationTask`] which can be used to cancel +//! managed effects. use async_channel::Receiver; use crate::{ @@ -12,7 +16,7 @@ pub(crate) struct CancellationTask { } impl CancellationTask { - pub(super) fn new(cancel_rx: Receiver, id: String) -> Self { + pub fn new(cancel_rx: Receiver, id: String) -> Self { Self { cancel_rx, id } } diff --git a/src/core/event_engine/effect_dispatcher.rs b/src/core/event_engine/effect_dispatcher.rs index c39047bc..fa3acc25 100644 --- a/src/core/event_engine/effect_dispatcher.rs +++ b/src/core/event_engine/effect_dispatcher.rs @@ -73,7 +73,7 @@ where log::debug!("Received invocation: {}", invocation.id()); let effect = cloned_self.dispatch(&invocation); - let task_completition = completion.clone(); + let task_completion = completion.clone(); if let Some(effect) = effect { log::debug!("Dispatched effect: {}", effect.id()); @@ -86,7 +86,7 @@ where cloned_self.remove_managed_effect(effect.id()); } - task_completition(events); + task_completion(events); }); } } @@ -260,7 +260,7 @@ mod should { "Non managed effects shouldn't be stored" ); - assert!(effect.unwrap().id() == "EFFECT_ONE"); + assert_eq!(effect.unwrap().id(), "EFFECT_ONE"); } #[tokio::test] @@ -275,7 +275,7 @@ mod should { "Managed effect should be removed on completion" ); - assert!(effect.unwrap().id() == "EFFECT_TWO"); + assert_eq!(effect.unwrap().id(), "EFFECT_TWO"); } #[test] @@ -283,7 +283,7 @@ mod should { let (_tx, rx) = async_channel::bounded::(5); let dispatcher = Arc::new(EffectDispatcher::new(TestEffectHandler {}, rx)); dispatcher.dispatch(&TestInvocation::Three); - let cancelation_effect = dispatcher.dispatch(&TestInvocation::CancelThree); + let cancellation_effect = dispatcher.dispatch(&TestInvocation::CancelThree); assert_eq!( dispatcher.managed.read().len(), @@ -291,6 +291,6 @@ mod should { "Managed effect should be cancelled" ); - assert!(cancelation_effect.is_none()); + assert!(cancellation_effect.is_none()); } } diff --git a/src/core/event_engine/mod.rs b/src/core/event_engine/mod.rs index ca99339c..e4394f6b 100644 --- a/src/core/event_engine/mod.rs +++ b/src/core/event_engine/mod.rs @@ -1,10 +1,11 @@ //! Event Engine module -use crate::lib::alloc::sync::Arc; use async_channel::Sender; use log::error; use spin::rwlock::RwLock; +use crate::{core::runtime::Runtime, lib::alloc::sync::Arc}; + #[doc(inline)] pub(crate) use effect::Effect; pub(crate) mod effect; @@ -29,12 +30,14 @@ pub(crate) mod event; pub(crate) use state::State; pub(crate) mod state; -use crate::core::runtime::Runtime; #[doc(inline)] pub(crate) use transition::Transition; - pub(crate) mod transition; +#[doc(inline)] +pub(crate) use cancel::CancellationTask; +pub(crate) mod cancel; + /// State machine's event engine. /// /// [`EventEngine`] is the core of state machines used in PubNub client and @@ -214,8 +217,8 @@ mod should { .exit() .unwrap_or(vec![]) .into_iter() - .chain(invocations.unwrap_or(vec![]).into_iter()) - .chain(state.enter().unwrap_or(vec![]).into_iter()) + .chain(invocations.unwrap_or(vec![])) + .chain(state.enter().unwrap_or(vec![])) .collect(), state, } diff --git a/src/core/mod.rs b/src/core/mod.rs index 4635b187..da33060d 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -13,11 +13,13 @@ pub use error::PubNubError; pub mod error; -#[cfg(any(feature = "publish", feature = "access"))] -#[doc(inline)] -pub(crate) use error_response::APIErrorBody; -#[cfg(any(feature = "publish", feature = "access"))] -pub(crate) mod error_response; +#[cfg(any( + feature = "publish", + feature = "access", + feature = "subscribe", + feature = "presence" +))] +pub(crate) mod service_response; #[cfg(feature = "blocking")] #[doc(inline)] diff --git a/src/core/runtime.rs b/src/core/runtime.rs index a628a18c..43864c7b 100644 --- a/src/core/runtime.rs +++ b/src/core/runtime.rs @@ -3,7 +3,14 @@ //! The [`Spawner`] trait is used to spawn async tasks in work of the PubNub //! client. -use crate::lib::{alloc::boxed::Box, core::future::Future}; +use crate::lib::{ + alloc::{ + fmt::{Debug, Formatter, Result}, + sync::Arc, + }, + core::future::Future, +}; +use futures::future::{BoxFuture, FutureExt}; /// PubNub spawner trait. /// @@ -45,3 +52,51 @@ pub trait Runtime: Clone + Send { /// Sleep current task for specified amount of time (in seconds). async fn sleep(self, delay: u64); } + +#[derive(Clone)] +pub(crate) struct RuntimeSupport { + spawner: Arc) + Send + Sync>, + sleeper: Arc BoxFuture<'static, ()> + Send + Sync>, +} + +impl RuntimeSupport { + pub fn new(runtime: Arc) -> Self + where + R: Runtime + Copy + Send + Sync + 'static, + { + let spawn_runtime = runtime.clone(); + let sleep_runtime = runtime.clone(); + + Self { + sleeper: Arc::new(move |delay| sleep_runtime.sleep(delay).boxed()), + spawner: Arc::new(Box::new(move |future| { + spawn_runtime.spawn(future); + })), + } + } +} + +#[async_trait::async_trait] +impl Runtime for RuntimeSupport { + fn spawn(&self, future: impl Future + Send + 'static) + where + R: Send + 'static, + { + (self.spawner.clone())( + async move { + future.await; + } + .boxed(), + ); + } + + async fn sleep(self, delay: u64) { + (self.sleeper)(delay).await + } +} + +impl Debug for RuntimeSupport { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "RuntimeSupport {{}}") + } +} diff --git a/src/core/error_response.rs b/src/core/service_response.rs similarity index 87% rename from src/core/error_response.rs rename to src/core/service_response.rs index bfd6b546..70c389d6 100644 --- a/src/core/error_response.rs +++ b/src/core/service_response.rs @@ -1,94 +1,59 @@ -//! # Error response +//! [`PubNub`] service response payload module. //! -//! The module contains a result type that represents parsed service error -//! responses for [`PubNubError`] consumption. - -use crate::core::PubNubError; -use crate::lib::{ - alloc::{ - borrow::ToOwned, - format, - string::{String, ToString}, - vec::Vec, +//! This module contains [`APISuccessBody`], [`APISuccessBodyWithMessage`] and +//! [`APIErrorBody`] types which represent result of [`PubNub`] network API +//! endpoint call. +//! +//! [`PubNub`]:https://www.pubnub.com/ + +use crate::{ + core::PubNubError, + lib::{ + alloc::{ + borrow::ToOwned, + format, + string::{String, ToString}, + vec::Vec, + }, + collections::HashMap, }, - collections::HashMap, }; -/// Implementation for [`APIError`] to create struct from service error response -/// body. -impl From for PubNubError { - fn from(value: APIErrorBody) -> Self { - PubNubError::API { - status: value.status(), - message: value.message(), - service: value.service(), - affected_channels: value.affected_channels(), - affected_channel_groups: value.affected_channel_groups(), - response: None, - } - } -} - -/// Additional error information struct. +/// Result of successful REST API endpoint call. /// -/// This structure used by [`AsObjectWithServiceAndErrorPayload`] to represent -/// list of errors in response. +/// Body contains status code, response `data` and `service` response specific +/// to used endpoint. #[cfg_attr(feature = "serde", derive(serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ErrorObjectPayload { - /// The list of channels for which an error was reported. - channels: Option>, - - /// The list of channel groups for which an error was reported. - #[cfg_attr(feature = "serde", serde(rename = "channel-groups"))] - channel_groups: Option>, +pub struct APISuccessBody { + pub status: i32, + pub data: D, + pub service: String, } -/// Additional error information struct. +/// Result of successful REST API endpoint call. /// -/// This structure used by [`ErrorObjectWithDetails`] to represent list of -/// errors in response. +/// Body contains status code, `message, response `payload` and `service` +/// response specific to used endpoint. #[cfg_attr(feature = "serde", derive(serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct ErrorObjectDetails { - /// A message explaining what went wrong. - message: String, - - /// Which part of the request caused an issue. - location: String, - - /// Type of issue reason. - #[cfg_attr(feature = "serde", serde(rename(deserialize = "locationType")))] - location_type: String, +pub struct APISuccessBodyWithPayload { + pub status: i32, + pub message: String, + pub payload: D, + pub service: String, } -/// Error description. +/// Result of successful REST API endpoint call. /// -/// This structure used by [`AsObjectWithErrorObject`] to represent list of -/// errors in response. +/// Body contains status code, `message` and `service` response specific to used +/// endpoint. #[cfg_attr(feature = "serde", derive(serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ErrorObject { - /// A message explaining what went wrong. - message: String, - - /// Service / sub-system which reported an error. - source: String, -} - -/// This structure used by [`APIErrorBody::AsObjectWithErrorObjectDetails`] to -/// represent server error response. -#[cfg_attr(feature = "serde", derive(serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ErrorObjectWithDetails { - /// A message explaining what went wrong. - message: String, - - /// Service / sub-system which reported an error. - source: String, - - /// Additional information about failure reasons. - details: Vec, +pub struct APISuccessBodyWithMessage { + pub status: i32, + pub message: String, + pub service: String, } /// PubNub service error response. @@ -301,6 +266,68 @@ pub enum APIErrorBody { }, } +/// Additional error information struct. +/// +/// This structure used by [`AsObjectWithServiceAndErrorPayload`] to represent +/// list of errors in response. +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ErrorObjectPayload { + /// The list of channels for which an error was reported. + channels: Option>, + + /// The list of channel groups for which an error was reported. + #[cfg_attr(feature = "serde", serde(rename = "channel-groups"))] + channel_groups: Option>, +} + +/// Additional error information struct. +/// +/// This structure used by [`ErrorObjectWithDetails`] to represent list of +/// errors in response. +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct ErrorObjectDetails { + /// A message explaining what went wrong. + message: String, + + /// Which part of the request caused an issue. + location: String, + + /// Type of issue reason. + #[cfg_attr(feature = "serde", serde(rename(deserialize = "locationType")))] + location_type: String, +} + +/// Error description. +/// +/// This structure used by [`AsObjectWithErrorObject`] to represent list of +/// errors in response. +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ErrorObject { + /// A message explaining what went wrong. + message: String, + + /// Service / sub-system which reported an error. + source: String, +} + +/// This structure used by [`APIErrorBody::AsObjectWithErrorObjectDetails`] to +/// represent server error response. +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ErrorObjectWithDetails { + /// A message explaining what went wrong. + message: String, + + /// Service / sub-system which reported an error. + source: String, + + /// Additional information about failure reasons. + details: Vec, +} + impl APIErrorBody { /// Retrieve status code from error body payload. fn status(&self) -> u16 { @@ -398,3 +425,18 @@ impl APIErrorBody { } } } + +/// Implementation for [`APIError`] to create struct from service error response +/// body. +impl From for PubNubError { + fn from(value: APIErrorBody) -> Self { + PubNubError::API { + status: value.status(), + message: value.message(), + service: value.service(), + affected_channels: value.affected_channels(), + affected_channel_groups: value.affected_channel_groups(), + response: None, + } + } +} diff --git a/src/core/transport.rs b/src/core/transport.rs index b52b621a..89e9e6fd 100644 --- a/src/core/transport.rs +++ b/src/core/transport.rs @@ -1,6 +1,7 @@ //! # Transport module //! -//! This module contains the [`Transport`] trait and the [`TransportRequest`] and [`TransportResponse`] types. +//! This module contains the [`Transport`] trait and the [`TransportRequest`] +//! and [`TransportResponse`] types. //! //! You can implement this trait for your own types, or use one of the provided //! features to use a transport library. @@ -46,7 +47,8 @@ pub trait Transport: Send + Sync { /// Send a request to the [`PubNub API`]. /// /// # Errors - /// Should return an [`PubNubError::Transport`] if the request cannot be sent. + /// Should return an [`PubNubError::Transport`] if the request cannot be + /// sent. /// /// [`PubNub API`]: https://www.pubnub.com/docs async fn send(&self, req: TransportRequest) -> Result; @@ -56,10 +58,11 @@ pub trait Transport: Send + Sync { pub mod blocking { //! # Blocking transport module //! - //! This module contains the [`Transport`] trait and the [`TransportRequest`] and [`TransportResponse`] types. + //! This module contains the [`Transport`] trait and the + //! [`TransportRequest`] and [`TransportResponse`] types. //! - //! You can implement this trait for your own types, or use one of the provided - //! features to use a transport library. + //! You can implement this trait for your own types, or use one of the + //! provided features to use a transport library. //! //! This trait is used for blocking requests. //! @@ -69,8 +72,8 @@ pub mod blocking { /// This trait is used to send requests to the [`PubNub API`]. /// - /// You can implement this trait for your own types, or use one of the provided - /// features to use a transport library. + /// You can implement this trait for your own types, or use one of the + /// provided features to use a transport library. /// /// This trait is used for blocking requests. /// @@ -94,7 +97,8 @@ pub mod blocking { /// Send a request to the [`PubNub API`]. /// /// # Errors - /// Should return an [`PubNubError::Transport`] if the request cannot be sent. + /// Should return an [`PubNubError::Transport`] if the request cannot be + /// sent. /// /// [`PubNub API`]: https://www.pubnub.com/docs fn send(&self, req: TransportRequest) -> Result; diff --git a/src/core/transport_request.rs b/src/core/transport_request.rs index e0d507ef..77f6cc0a 100644 --- a/src/core/transport_request.rs +++ b/src/core/transport_request.rs @@ -7,12 +7,21 @@ //! //! [`pubnub`]: ../index.html -use crate::lib::{ - alloc::{string::String, vec::Vec}, - collections::HashMap, - core::fmt::{Display, Formatter, Result}, +use crate::{ + core::PubNubError, + lib::{ + alloc::{ + boxed::Box, + sync::Arc, + {string::String, vec::Vec}, + }, + collections::HashMap, + core::fmt::{Display, Formatter}, + }, }; +type DeserializerClosure = Box Result>; + /// The method to use for a request. /// /// This enum represents the method to use for a request. It is used by the @@ -33,7 +42,7 @@ pub enum TransportMethod { } impl Display for TransportMethod { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { write!( f, "{}", @@ -72,3 +81,137 @@ pub struct TransportRequest { /// body to be sent with the request pub body: Option>, } + +impl TransportRequest { + /// Send async request and process [`PubNub API`] response. + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + #[cfg(not(feature = "serde"))] + pub(crate) async fn send( + &self, + transport: &T, + deserializer: Arc, + ) -> Result + where + B: for<'de> super::Deserialize<'de>, + R: TryFrom, + T: super::Transport, + D: super::Deserializer + 'static, + { + // Request configured endpoint. + let response = transport.send(self.clone()).await?; + Self::deserialize( + response.clone(), + Box::new(move |bytes| deserializer.deserialize(bytes)), + ) + } + + /// Send async request and process [`PubNub API`] response. + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + #[cfg(feature = "serde")] + pub(crate) async fn send( + &self, + transport: &T, + deserializer: Arc, + ) -> Result + where + B: for<'de> serde::Deserialize<'de>, + R: TryFrom, + T: super::Transport, + D: super::Deserializer + 'static, + { + // Request configured endpoint. + let response = transport.send(self.clone()).await?; + Self::deserialize( + response.clone(), + Box::new(move |bytes| deserializer.deserialize(bytes)), + ) + } + /// Send async request and process [`PubNub API`] response. + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + #[cfg(all(not(feature = "serde"), feature = "blocking"))] + pub(crate) fn send_blocking( + &self, + transport: &T, + deserializer: Arc, + ) -> Result + where + B: for<'de> super::Deserialize<'de>, + R: TryFrom, + T: super::blocking::Transport, + D: super::Deserializer + 'static, + { + // Request configured endpoint. + let response = transport.send(self.clone())?; + Self::deserialize( + response.clone(), + Box::new(move |bytes| deserializer.deserialize(bytes)), + ) + } + + /// Send blocking request and process [`PubNub API`] response. + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + #[cfg(all(feature = "serde", feature = "blocking"))] + pub(crate) fn send_blocking( + &self, + transport: &T, + deserializer: Arc, + ) -> Result + where + B: for<'de> serde::Deserialize<'de>, + R: TryFrom, + T: super::blocking::Transport, + D: super::Deserializer + 'static, + { + // Request configured endpoint. + let response = transport.send(self.clone())?; + Self::deserialize( + response.clone(), + Box::new(move |bytes| deserializer.deserialize(bytes)), + ) + } + + /// Deserialize [`PubNub API`] response. + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + fn deserialize( + response: super::TransportResponse, + des: DeserializerClosure, + ) -> Result + where + R: TryFrom, + { + response + .clone() + .body + .map(|bytes| { + let deserialize_result = des(&bytes); + if deserialize_result.is_err() && response.status >= 500 { + Err(PubNubError::general_api_error( + "Unexpected service response", + None, + Some(Box::new(response.clone())), + )) + } else { + deserialize_result + } + }) + .map_or( + Err(PubNubError::general_api_error( + "No body in the response!", + None, + Some(Box::new(response.clone())), + )), + |response_body| { + response_body.and_then::(|body: B| { + body.try_into().map_err(|response_error: PubNubError| { + response_error.attach_response(response) + }) + }) + }, + ) + } +} diff --git a/src/core/utils/encoding.rs b/src/core/utils/encoding.rs index b33eaca4..8dff339c 100644 --- a/src/core/utils/encoding.rs +++ b/src/core/utils/encoding.rs @@ -38,15 +38,15 @@ pub enum UrlEncodeExtension { } /// `percent_encoding` crate recommends you to create your own set for encoding. -/// To be consistent in the whole codebase - we created a function that can be used -/// for encoding related stuff. +/// To be consistent in the whole codebase - we created a function that can be +/// used for encoding related stuff. pub fn url_encode(data: &[u8]) -> String { url_encode_extended(data, UrlEncodeExtension::Default).to_string() } /// `percent_encoding` crate recommends you to create your own set for encoding. -/// To be consistent in the whole codebase - we created a function that can be used -/// for encoding related stuff. +/// To be consistent in the whole codebase - we created a function that can be +/// used for encoding related stuff. pub fn url_encode_extended(data: &[u8], extension: UrlEncodeExtension) -> String { let set = match extension { UrlEncodeExtension::Default => PUBNUB_SET, @@ -70,3 +70,33 @@ pub fn join_url_encoded(strings: &[&str], sep: &str) -> Option { .join(sep), ) } + +/// URL-encode channels list. +/// +/// Channels list used as part of URL path and therefore required. +#[cfg(any(feature = "subscribe", feature = "presence"))] +pub(crate) fn url_encoded_channels(channels: &[String]) -> String { + join_url_encoded( + channels + .iter() + .map(|v| v.as_str()) + .collect::>() + .as_slice(), + ",", + ) + .unwrap_or(",".into()) +} + +/// URL-encode channel groups list. +#[cfg(any(feature = "subscribe", feature = "presence"))] +pub(crate) fn url_encoded_channel_groups(channel_groups: &[String]) -> Option { + join_url_encoded( + channel_groups + .iter() + .map(|v| v.as_str()) + .collect::>() + .as_slice(), + ",", + ) + .filter(|string| !string.is_empty()) +} diff --git a/src/core/utils/mod.rs b/src/core/utils/mod.rs index 28a771f6..0a8a0625 100644 --- a/src/core/utils/mod.rs +++ b/src/core/utils/mod.rs @@ -1,6 +1,16 @@ -#[cfg(any(feature = "publish", feature = "access"))] +#[cfg(any( + feature = "publish", + feature = "access", + feature = "subscribe", + feature = "presence" +))] pub mod encoding; -#[cfg(any(feature = "publish", feature = "access"))] +#[cfg(any( + feature = "publish", + feature = "access", + feature = "subscribe", + feature = "presence" +))] pub mod headers; pub mod metadata; diff --git a/src/dx/access/builders/grant_token.rs b/src/dx/access/builders/grant_token.rs index 5fe96dee..ed088fe6 100644 --- a/src/dx/access/builders/grant_token.rs +++ b/src/dx/access/builders/grant_token.rs @@ -34,20 +34,17 @@ use derive_builder::Builder; pub struct GrantTokenRequest<'pa, T, S, D> where S: for<'se, 'rq> Serializer<'se, GrantTokenPayload<'rq>>, - D: Deserializer, { /// Current client which can provide transportation to perform the request. + /// + /// This field is used to get [`Transport`] to perform the request. #[builder(field(vis = "pub(in crate::dx::access)"), setter(custom))] - pub(in crate::dx::access) pubnub_client: PubNubClientInstance, + pub(in crate::dx::access) pubnub_client: PubNubClientInstance, /// Request payload serializer. #[builder(field(vis = "pub(in crate::dx::access)"), setter(custom))] pub(in crate::dx::access) serializer: S, - /// Service response deserializer. - #[builder(field(vis = "pub(in crate::dx::access)"), setter(custom))] - pub(in crate::dx::access) deserializer: D, - /// How long (in minutes) the generated token should be valid. #[builder(field(vis = "pub(in crate::dx::access)"), setter(custom))] pub(in crate::dx::access) ttl: usize, @@ -97,42 +94,17 @@ where /// /// [`PubNub`]:https://www.pubnub.com/ #[cfg(not(feature = "serde"))] -pub struct GrantTokenRequestWithSerializerBuilder { +pub struct GrantTokenRequestWithSerializerBuilder { /// Current client which can provide transportation to perform the request. - pub(in crate::dx::access) pubnub_client: PubNubClientInstance, + pub(in crate::dx::access) pubnub_client: PubNubClientInstance, /// How long (in minutes) the generated token should be valid. pub(in crate::dx::access) ttl: usize, } -/// The [`GrantTokenRequestWithDeserializerBuilder`] is used to build grant access -/// token permissions to access specific resource endpoints request that is sent -/// to the [`PubNub`] network. -/// -/// This struct used by the [`grant_token`] method of the [`PubNubClient`] and -/// let specify custom deserializer for [`PubNub`] network response. -/// The [`grant_token`] method is used to generate access token. -/// -/// [`PubNub`]:https://www.pubnub.com/ -#[cfg(not(feature = "serde"))] -pub struct GrantTokenRequestWithDeserializerBuilder -where - S: for<'se, 'rq> Serializer<'se, GrantTokenPayload<'rq>>, -{ - /// Current client which can provide transportation to perform the request. - pubnub_client: PubNubClientInstance, - - /// How long (in minutes) the generated token should be valid. - ttl: usize, - - /// Request payload serializer. - serializer: Option, -} - impl<'pa, T, S, D> GrantTokenRequest<'pa, T, S, D> where S: for<'se, 'rq> Serializer<'se, GrantTokenPayload<'rq>>, - D: Deserializer, { /// Create transport request from the request builder. pub(in crate::dx::access) fn transport_request(&self) -> TransportRequest { @@ -153,7 +125,6 @@ where impl<'pa, T, S, D> GrantTokenRequestBuilder<'pa, T, S, D> where S: for<'se, 'rq> Serializer<'se, GrantTokenPayload<'rq>>, - D: Deserializer, { /// Validate user-provided data for request builder. /// @@ -168,49 +139,20 @@ impl<'pa, T, S, D> GrantTokenRequestBuilder<'pa, T, S, D> where T: Transport, S: for<'se, 'rq> Serializer<'se, GrantTokenPayload<'rq>>, - D: Deserializer, + D: Deserializer + 'static, { - /// Build and call request. + /// Build and call asynchronous request. pub async fn execute(self) -> Result { - // Build request instance and report errors if any. let request = self .build() .map_err(|err| PubNubError::general_api_error(err.to_string(), None, None))?; let transport_request = request.transport_request(); let client = request.pubnub_client.clone(); - let deserializer = request.deserializer; - - let response = client.transport.send(transport_request).await?; - response - .clone() - .body - .map(|bytes| { - let deserialize_result = deserializer.deserialize(&bytes); - if deserialize_result.is_err() && response.status >= 500 { - Err(PubNubError::general_api_error( - "Unexpected service response", - None, - Some(Box::new(response.clone())), - )) - } else { - deserialize_result - } - }) - .map_or( - Err(PubNubError::general_api_error( - "No body in the response!", - None, - Some(Box::new(response.clone())), - )), - |response_body| { - response_body.and_then::(|body| { - body.try_into().map_err(|response_error: PubNubError| { - response_error.attach_response(response) - }) - }) - }, - ) + let deserializer = client.deserializer.clone(); + transport_request + .send::(&client.transport, deserializer) + .await } } @@ -219,9 +161,9 @@ impl<'pa, T, S, D> GrantTokenRequestBuilder<'pa, T, S, D> where T: crate::core::blocking::Transport, S: for<'se, 'rq> Serializer<'se, GrantTokenPayload<'rq>>, - D: Deserializer, + D: Deserializer + 'static, { - /// Execute the request and return the result. + /// Execute synchronous request and return the result. /// /// This method is synchronous and will return result which will resolve to /// a [`RevokeTokenResult`] or [`PubNubError`]. @@ -262,86 +204,30 @@ where let transport_request = request.transport_request(); let client = request.pubnub_client.clone(); - let deserializer = request.deserializer; - - let response = client.transport.send(transport_request)?; - response - .body - .as_ref() - .map(|bytes| { - let deserialize_result = deserializer.deserialize(bytes); - if deserialize_result.is_err() && response.status >= 500 { - Err(PubNubError::general_api_error( - "Unexpected service response", - None, - Some(Box::new(response.clone())), - )) - } else { - deserialize_result - } - }) - .map_or( - Err(PubNubError::general_api_error( - "No body in the response!", - None, - Some(Box::new(response.clone())), - )), - |response_body| { - response_body.and_then::(|body| { - body.try_into().map_err(|response_error: PubNubError| { - response_error.attach_response(response) - }) - }) - }, - ) + let deserializer = client.deserializer.clone(); + transport_request + .send_blocking::(&client.transport, deserializer) } } #[cfg(not(feature = "serde"))] -impl GrantTokenRequestWithSerializerBuilder { +impl GrantTokenRequestWithSerializerBuilder { /// Add custom serializer. /// /// Adds the serializer to the [`GrantTokenRequestBuilder`]. /// /// Instance of [`GrantTokenRequestWithDeserializerBuilder`] returned. - pub fn serialize_with( + pub fn serialize_with<'request, S>( self, serializer: S, - ) -> GrantTokenRequestWithDeserializerBuilder + ) -> GrantTokenRequestBuilder<'request, T, S, D> where - D: Deserializer, S: for<'se, 'rq> Serializer<'se, GrantTokenPayload<'rq>>, - { - GrantTokenRequestWithDeserializerBuilder { - pubnub_client: self.pubnub_client, - ttl: self.ttl, - serializer: Some(serializer), - } - } -} - -#[cfg(not(feature = "serde"))] -impl GrantTokenRequestWithDeserializerBuilder -where - S: for<'se, 'rq> Serializer<'se, GrantTokenPayload<'rq>>, -{ - /// Add custom deserializer. - /// - /// Adds the deserializer to the [`GrantTokenRequestBuilder`]. - /// - /// Instance of [`GrantTokenRequestBuilder`] returned. - pub fn deserialize_with<'builder, D>( - self, - deserializer: D, - ) -> GrantTokenRequestBuilder<'builder, T, S, D> - where - D: Deserializer, { GrantTokenRequestBuilder { pubnub_client: Some(self.pubnub_client), ttl: Some(self.ttl), - serializer: self.serializer, - deserializer: Some(deserializer), + serializer: Some(serializer), ..Default::default() } } diff --git a/src/dx/access/builders/mod.rs b/src/dx/access/builders/mod.rs index c80d8f8d..b09a61b3 100644 --- a/src/dx/access/builders/mod.rs +++ b/src/dx/access/builders/mod.rs @@ -2,32 +2,26 @@ //! //! This module contains all builders for the PAM management operations. +use crate::{dx::pubnub_client::PubNubClientInstance, lib::alloc::string::String}; + #[doc(inline)] pub use grant_token::{GrantTokenRequest, GrantTokenRequestBuilder}; #[cfg(not(feature = "serde"))] #[doc(inline)] -pub use grant_token::{ - GrantTokenRequestWithDeserializerBuilder, GrantTokenRequestWithSerializerBuilder, -}; +pub use grant_token::GrantTokenRequestWithSerializerBuilder; pub mod grant_token; -#[cfg(not(feature = "serde"))] -#[doc(inline)] -pub use revoke::RevokeTokenRequestWithDeserializerBuilder; #[doc(inline)] pub use revoke::{RevokeTokenRequest, RevokeTokenRequestBuilder}; pub mod revoke; -use crate::dx::pubnub_client::PubNubClientInstance; -use crate::lib::alloc::string::String; - /// Validate [`PubNubClient`] configuration. /// /// Check whether if the [`PubNubConfig`] contains all the required fields set /// for PAM endpoint usage or not. -pub(in crate::dx::access::builders) fn validate_configuration( - client: &Option>, +pub(in crate::dx::access::builders) fn validate_configuration( + client: &Option>, ) -> Result<(), String> { if let Some(client) = client { if client.config.subscribe_key.is_empty() { diff --git a/src/dx/access/builders/revoke.rs b/src/dx/access/builders/revoke.rs index 4708bdb7..e63e399f 100644 --- a/src/dx/access/builders/revoke.rs +++ b/src/dx/access/builders/revoke.rs @@ -12,7 +12,7 @@ use crate::{ Deserializer, Transport, TransportMethod, TransportRequest, }, dx::{access::*, pubnub_client::PubNubClientInstance}, - lib::alloc::{boxed::Box, format, string::ToString}, + lib::alloc::{format, string::ToString}, }; use derive_builder::Builder; @@ -32,45 +32,19 @@ use derive_builder::Builder; /// [`PubNub`]:https://www.pubnub.com/ /// [`revoke_token`]: crate::dx::PubNubClient::revoke_token /// [`PubNubClient`]: crate::PubNubClient -pub struct RevokeTokenRequest -where - D: Deserializer, -{ +pub struct RevokeTokenRequest { /// Current client which can provide transportation to perform the request. + /// + /// This field is used to get [`Transport`] to perform the request. #[builder(field(vis = "pub(in crate::dx::access)"), setter(custom))] - pub(in crate::dx::access) pubnub_client: PubNubClientInstance, - - /// Service response deserializer. - #[builder(field(vis = "pub(in crate::dx::access)"), setter(custom))] - pub(super) deserializer: D, + pub(in crate::dx::access) pubnub_client: PubNubClientInstance, /// Access token for which permissions should be revoked. #[builder(field(vis = "pub(in crate::dx::access)"), setter(custom))] pub(super) token: String, } -/// The [`RevokeTokenRequestWithDeserializerBuilder`] is used to build revoke -/// access token permissions to access specific resource endpoints request that -/// is sent to the [`PubNub`] network. -/// -/// This struct used by the [`revoke_token`] method of the [`PubNubClient`] and -/// let specify custom deserializer for [`PubNub`] network response. -/// The [`revoke_token`] method is used to revoke access token permissions. -/// -/// [`PubNub`]:https://www.pubnub.com/ -#[cfg(not(feature = "serde"))] -pub struct RevokeTokenRequestWithDeserializerBuilder { - /// Current client which can provide transportation to perform the request. - pub(in crate::dx::access) pubnub_client: PubNubClientInstance, - - /// Access token for which permissions should be revoked. - pub token: String, -} - -impl RevokeTokenRequest -where - D: Deserializer, -{ +impl RevokeTokenRequest { /// Create transport request from the request builder. pub(in crate::dx::access) fn transport_request(&self) -> TransportRequest { let sub_key = &self.pubnub_client.config.subscribe_key; @@ -87,10 +61,7 @@ where } } -impl RevokeTokenRequestBuilder -where - D: Deserializer, -{ +impl RevokeTokenRequestBuilder { /// Validate user-provided data for request builder. /// /// Validator ensure that list of provided data is enough to build valid @@ -103,9 +74,9 @@ where impl RevokeTokenRequestBuilder where T: Transport, - D: Deserializer, + D: Deserializer + 'static, { - /// Build and call request. + /// Build and call asynchronous request. pub async fn execute(self) -> Result { // Build request instance and report errors if any. let request = self @@ -114,38 +85,10 @@ where let transport_request = request.transport_request(); let client = request.pubnub_client.clone(); - let deserializer = request.deserializer; - - let response = client.transport.send(transport_request).await?; - response - .clone() - .body - .map(|bytes| { - let deserialize_result = deserializer.deserialize(&bytes); - if deserialize_result.is_err() && response.status >= 500 { - Err(PubNubError::general_api_error( - "Unexpected service response", - None, - Some(Box::new(response.clone())), - )) - } else { - deserialize_result - } - }) - .map_or( - Err(PubNubError::general_api_error( - "No body in the response!", - None, - Some(Box::new(response.clone())), - )), - |response_body| { - response_body.and_then::(|body| { - body.try_into().map_err(|response_error: PubNubError| { - response_error.attach_response(response) - }) - }) - }, - ) + let deserializer = client.deserializer.clone(); + transport_request + .send::(&client.transport, deserializer) + .await } } @@ -153,9 +96,9 @@ where impl RevokeTokenRequestBuilder where T: crate::core::blocking::Transport, - D: Deserializer, + D: Deserializer + 'static, { - /// Execute the request and return the result. + /// Execute synchronous request and return the result. /// /// This method is synchronous and will return result which will resolve to /// a [`RevokeTokenResult`] or [`PubNubError`]. @@ -189,56 +132,8 @@ where let transport_request = request.transport_request(); let client = request.pubnub_client.clone(); - let deserializer = request.deserializer; - - let response = client.transport.send(transport_request)?; - response - .body - .as_ref() - .map(|bytes| { - let deserialize_result = deserializer.deserialize(bytes); - if deserialize_result.is_err() && response.status >= 500 { - Err(PubNubError::general_api_error( - "Unexpected service response", - None, - Some(Box::new(response.clone())), - )) - } else { - deserialize_result - } - }) - .map_or( - Err(PubNubError::general_api_error( - "No body in the response!", - None, - Some(Box::new(response.clone())), - )), - |response_body| { - response_body.and_then::(|body| { - body.try_into().map_err(|response_error: PubNubError| { - response_error.attach_response(response) - }) - }) - }, - ) - } -} - -#[cfg(not(feature = "serde"))] -impl RevokeTokenRequestWithDeserializerBuilder { - /// Add custom deserializer. - /// - /// Adds the deserializer to the [`RevokeTokenRequestBuilder`]. - /// - /// Instance of [`RevokeTokenRequestBuilder`] returned. - pub fn deserialize_with(self, deserializer: D) -> RevokeTokenRequestBuilder - where - D: Deserializer, - { - RevokeTokenRequestBuilder { - pubnub_client: Some(self.pubnub_client), - token: Some(self.token), - deserializer: Some(deserializer), - } + let deserializer = client.deserializer.clone(); + transport_request + .send_blocking::(&client.transport, deserializer) } } diff --git a/src/dx/access/mod.rs b/src/dx/access/mod.rs index 208b9de3..1f666ffe 100644 --- a/src/dx/access/mod.rs +++ b/src/dx/access/mod.rs @@ -36,11 +36,9 @@ pub mod permissions; use crate::dx::pubnub_client::PubNubClientInstance; use crate::lib::alloc::string::String; #[cfg(feature = "serde")] -use crate::providers::{ - deserialization_serde::SerdeDeserializer, serialization_serde::SerdeSerializer, -}; +use crate::providers::serialization_serde::SerdeSerializer; -impl PubNubClientInstance { +impl PubNubClientInstance { /// Create grant token permissions request builder. /// This method is used to generate token with required permissions. /// @@ -79,14 +77,10 @@ impl PubNubClientInstance { /// # } /// ``` #[cfg(feature = "serde")] - pub fn grant_token( - &self, - ttl: usize, - ) -> GrantTokenRequestBuilder { + pub fn grant_token(&self, ttl: usize) -> GrantTokenRequestBuilder { GrantTokenRequestBuilder { pubnub_client: Some(self.clone()), serializer: Some(SerdeSerializer), - deserializer: Some(SerdeDeserializer), ttl: Some(ttl), ..Default::default() } @@ -150,7 +144,7 @@ impl PubNubClientInstance { /// # } /// ``` #[cfg(not(feature = "serde"))] - pub fn grant_token(&self, ttl: usize) -> GrantTokenRequestWithSerializerBuilder { + pub fn grant_token(&self, ttl: usize) -> GrantTokenRequestWithSerializerBuilder { GrantTokenRequestWithSerializerBuilder { pubnub_client: self.clone(), ttl, @@ -188,70 +182,15 @@ impl PubNubClientInstance { /// # Ok(()) /// # } /// ``` - #[cfg(feature = "serde")] - pub fn revoke_token(&self, token: S) -> RevokeTokenRequestBuilder + pub fn revoke_token(&self, token: S) -> RevokeTokenRequestBuilder where S: Into, { RevokeTokenRequestBuilder { pubnub_client: Some(self.clone()), - deserializer: Some(SerdeDeserializer), token: Some(token.into()), } } - - /// Create revoke token permissions request builder. - /// This method is used to revoke token permissions. - /// - /// Instance of [`RevokeTokenRequestWithDeserializerBuilder`] returned. - /// - /// # Example - /// ```rust,no_run - /// use pubnub::{ - /// access::*, - /// core::{Deserializer, PubNubError, Serializer}, - /// # PubNubClientBuilder, Keyset, - /// }; - /// - /// struct MyDeserializer; - /// - /// impl<'de> Deserializer<'de, GrantTokenResponseBody> for MyDeserializer { - /// fn deserialize(&self, response: &'de [u8]) -> Result { - /// // ... - /// # Ok(GrantTokenResult) - /// } - /// } - /// - /// # - /// # #[tokio::main] - /// # async fn main() -> Result<(), Box> { - /// let mut pubnub = // PubNubClient - /// # PubNubClientBuilder::with_reqwest_transport() - /// # .with_keyset(Keyset { - /// # subscribe_key: "demo", - /// # publish_key: Some("demo"), - /// # secret_key: Some("demo") - /// # }) - /// # .with_user_id("uuid") - /// # .build()?; - /// pubnub - /// .revoke_token("p0F2AkF0Gl043r....Dc3BjoERtZXRhoENzaWdYIGOAeTyWGJI".into()) - /// .derialize_with(MyDeserializer) - /// .execute() - /// .await?; - /// # Ok(()) - /// # } - /// ``` - #[cfg(not(feature = "serde"))] - pub fn revoke_token(&self, token: S) -> RevokeTokenRequestWithDeserializerBuilder - where - S: Into, - { - RevokeTokenRequestWithDeserializerBuilder { - pubnub_client: self.clone(), - token: token.into(), - } - } } #[cfg(test)] @@ -264,6 +203,7 @@ mod it_should { alloc::{borrow::ToOwned, format, vec, vec::Vec}, collections::HashMap, }, + providers::deserialization_serde::DeserializerSerde, transport::middleware::PubNubMiddleware, {Keyset, PubNubClientBuilder}, }; @@ -333,7 +273,7 @@ mod it_should { with_auth_key: Option, with_auth_token: Option, transport: Option, - ) -> PubNubClientInstance> { + ) -> PubNubClientInstance, DeserializerSerde> { let mut builder = PubNubClientBuilder::with_transport(transport.unwrap_or(MockTransport { response: None, request_handler: None, diff --git a/src/dx/access/payloads.rs b/src/dx/access/payloads.rs index 9d90b4d4..64e3a70a 100644 --- a/src/dx/access/payloads.rs +++ b/src/dx/access/payloads.rs @@ -5,8 +5,8 @@ //! traits. use crate::{ - core::{Deserializer, Serializer}, - dx::access::{permissions::*, types::MetaValue, GrantTokenRequest, GrantTokenResponseBody}, + core::Serializer, + dx::access::{permissions::*, types::MetaValue, GrantTokenRequest}, lib::{ alloc::{ boxed::Box, @@ -82,7 +82,6 @@ impl<'request> GrantTokenPayload<'request> { pub(super) fn new(request: &'request GrantTokenRequest<'_, T, S, D>) -> Self where S: for<'se, 'rq> Serializer<'se, GrantTokenPayload<'rq>>, - D: Deserializer, { GrantTokenPayload { ttl: request.ttl, diff --git a/src/dx/access/result.rs b/src/dx/access/result.rs index b432a9ef..5cad6b2a 100644 --- a/src/dx/access/result.rs +++ b/src/dx/access/result.rs @@ -4,7 +4,10 @@ //! The [`GrantTokenResult`] type is used to represent results of access token //! generation operation. -use crate::core::{APIErrorBody, PubNubError}; +use crate::core::{ + service_response::{APIErrorBody, APISuccessBody}, + PubNubError, +}; use crate::lib::alloc::string::String; /// The result of a grant token operation. @@ -141,17 +144,6 @@ pub struct RevokeTokenResponseBodyPayload { message: String, } -/// Content of successful PAMv3 REST API operation. -/// -/// Body contains status code and `service` response specific to used endpoint. -#[cfg_attr(feature = "serde", derive(serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct APISuccessBody { - status: i32, - data: D, - service: String, -} - impl TryFrom for RevokeTokenResult { type Error = PubNubError; diff --git a/src/dx/mod.rs b/src/dx/mod.rs index 97b6c121..085333fa 100644 --- a/src/dx/mod.rs +++ b/src/dx/mod.rs @@ -15,6 +15,9 @@ pub mod publish; #[cfg(feature = "subscribe")] pub mod subscribe; +#[cfg(feature = "presence")] +pub mod presence; + #[cfg(all(feature = "parse_token", feature = "serde"))] pub use parse_token::parse_token; #[cfg(feature = "parse_token")] diff --git a/src/dx/parse_token.rs b/src/dx/parse_token.rs index d4322a2d..9cdbbc7b 100644 --- a/src/dx/parse_token.rs +++ b/src/dx/parse_token.rs @@ -22,8 +22,11 @@ use ciborium::de::from_reader; struct CiboriumDeserializer; #[cfg(feature = "serde")] -impl Deserializer for CiboriumDeserializer { - fn deserialize(&self, bytes: &[u8]) -> Result { +impl Deserializer for CiboriumDeserializer { + fn deserialize serde::Deserialize<'de>>( + &self, + bytes: &[u8], + ) -> Result { use crate::lib::core::ops::Deref; from_reader(bytes.deref()).map_err(|e| PubNubError::TokenDeserialization { @@ -42,12 +45,12 @@ pub fn parse_token(token: &str) -> Result { } /// The [`parse_token`] function decodes an existing token and returns the -/// struct deserialized by provided cbor deserializer containing permissions embedded in that token. -/// The client can use this method for debugging to check the permissions to the -/// resources. +/// struct deserialized by provided cbor deserializer containing permissions +/// embedded in that token. The client can use this method for debugging to +/// check the permissions to the resources. pub fn parse_token_with(token: &str, deserializer: D) -> Result where - D: Deserializer, + D: Deserializer, { let token_bytes = general_purpose::URL_SAFE .decode(format!("{token}{}", "=".repeat(token.len() % 4)).as_bytes()) diff --git a/src/dx/presence/builders/heartbeat.rs b/src/dx/presence/builders/heartbeat.rs new file mode 100644 index 00000000..6f8f5458 --- /dev/null +++ b/src/dx/presence/builders/heartbeat.rs @@ -0,0 +1,339 @@ +//! # PubNub heartbeat module. +//! +//! The [`HeartbeatRequestBuilder`] lets you to make and execute requests that +//! will announce specified `user_id` presence in the provided channels and +//! groups. + +use derive_builder::Builder; +#[cfg(feature = "std")] +use futures::{ + future::BoxFuture, + {select_biased, FutureExt}, +}; + +use crate::{ + core::{ + blocking, + utils::{ + encoding::{url_encoded_channel_groups, url_encoded_channels}, + headers::{APPLICATION_JSON, CONTENT_TYPE}, + }, + Deserializer, PubNubError, Serialize, Transport, TransportMethod, TransportRequest, + }, + dx::{ + presence::{builders, HeartbeatResponseBody, HeartbeatResult}, + pubnub_client::PubNubClientInstance, + }, + lib::{ + alloc::{ + string::{String, ToString}, + vec, + }, + collections::HashMap, + }, +}; + +#[cfg(feature = "std")] +use crate::{core::event_engine::cancel::CancellationTask, lib::alloc::sync::Arc}; + +/// The [`HeartbeatRequestsBuilder`] is used to build a `user_id` presence +/// announcement request that is sent to the [`PubNub`] network. +/// +/// This struct is used by the [`heartbeat`] and [`set_state_with_heartbeat`] +/// methods of the [`PubNubClient`]. +/// The [`heartbeat`] method is used to announce specified `user_id` presence in +/// the provided channels and groups. +/// The [`set_state_with_heartbeat`] is used to update the state associated with +/// `user_id` and announce its presence in the provided channels and groups. +/// +/// [`PubNub`]:https://www.pubnub.com/ +#[derive(Builder)] +#[builder( + pattern = "owned", + build_fn(vis = "pub(in crate::dx::presence)", validate = "Self::validate"), + no_std +)] +pub struct HeartbeatRequest { + /// Current client which can provide transportation to perform the request. + /// + /// This field is used to get [`Transport`] to perform the request. + #[builder(field(vis = "pub(in crate::dx::presence)"), setter(custom))] + pub(in crate::dx::presence) pubnub_client: PubNubClientInstance, + + /// Channel(s) for announcement. + #[builder( + field(vis = "pub(in crate::dx::presence)"), + setter(strip_option, into), + default = "vec![]" + )] + pub(in crate::dx::presence) channels: Vec, + + /// Channel group(s) for announcement. + #[builder( + field(vis = "pub(in crate::dx::presence)"), + setter(into, strip_option), + default = "vec![]" + )] + pub(in crate::dx::presence) channel_groups: Vec, + + /// A state that should be associated with the `user_id`. + /// + /// `state` object should be a `HashMap` with channel names as keys and + /// nested `HashMap` with values. State with heartbeat can be set **only** + /// for channels. + /// + /// # Example: + /// ```rust,no_run + /// # use std::collections::HashMap; + /// # fn main() { + /// let state = HashMap::>::from([( + /// "announce".into(), + /// HashMap::from([ + /// ("is_owner".into(), false), + /// ("is_admin".into(), true) + /// ]) + /// )]); + /// # } + /// ``` + #[builder( + field(vis = "pub(in crate::dx::presence)"), + setter(custom, strip_option) + )] + pub(in crate::dx::presence) state: Option>, + + /// `user_id`presence timeout period. + /// + /// A heartbeat is a period of time during which `user_id` is visible + /// `online`. + /// If, within the heartbeat period, another heartbeat request or a + /// subscribe (for an implicit heartbeat) request `timeout` will be + /// announced for `user_id`. + /// + /// By default it is set to **300** seconds. + #[builder( + field(vis = "pub(in crate::dx::presence)"), + setter(strip_option), + default = "300" + )] + pub(in crate::dx::presence) heartbeat: u64, + + /// Identifier for which presence in channels and/or channel groups will be + /// announced. + #[builder(field(vis = "pub(in crate::dx::presence)"), setter(strip_option, into))] + pub(in crate::dx::presence) user_id: String, +} + +impl HeartbeatRequestBuilder { + /// Validate user-provided data for request builder. + /// + /// Validator ensure that provided information is enough to build valid + /// heartbeat request instance. + fn validate(&self) -> Result<(), String> { + let groups_len = self.channel_groups.as_ref().map_or_else(|| 0, |v| v.len()); + let channels_len = self.channels.as_ref().map_or_else(|| 0, |v| v.len()); + + builders::validate_configuration(&self.pubnub_client).and_then(|_| { + if channels_len == groups_len && channels_len == 0 { + Err("Either channels or channel groups should be provided".into()) + } else if self.user_id.is_none() { + Err("User id is missing".into()) + } else { + Ok(()) + } + }) + } + + /// Build [`HeartbeatRequest`] from builder. + fn request(self) -> Result, PubNubError> { + self.build() + .map_err(|err| PubNubError::general_api_error(err.to_string(), None, None)) + } +} + +impl HeartbeatRequest { + /// Create transport request from the request builder. + pub(in crate::dx::presence) fn transport_request( + &self, + ) -> Result { + let sub_key = &self.pubnub_client.config.subscribe_key; + let mut query: HashMap = HashMap::new(); + query.insert("heartbeat".into(), self.heartbeat.to_string()); + query.insert("uuid".into(), self.user_id.to_string()); + + // Serialize list of channel groups and add into query parameters list. + url_encoded_channel_groups(&self.channel_groups) + .and_then(|groups| query.insert("channel-group".into(), groups)); + + if let Some(state) = &self.state { + let serialized_state = + String::from_utf8(state.clone()).map_err(|err| PubNubError::Serialization { + details: err.to_string(), + })?; + query.insert("state".into(), serialized_state); + } + + Ok(TransportRequest { + path: format!( + "/v2/presence/sub_key/{sub_key}/channel/{}/heartbeat", + url_encoded_channels(&self.channels) + ), + query_parameters: query, + method: TransportMethod::Get, + headers: [(CONTENT_TYPE.into(), APPLICATION_JSON.into())].into(), + body: None, + }) + } +} + +impl HeartbeatRequestBuilder { + /// A state that should be associated with the `user_id`. + /// + /// `state` object should be a `HashMap` with channel names as keys and + /// nested `HashMap` with values. State with heartbeat can be set **only** + /// for channels. + /// + /// # Example: + /// ```rust,no_run + /// # use std::collections::HashMap; + /// # fn main() { + /// let state = HashMap::>::from([( + /// "announce".into(), + /// HashMap::from([ + /// ("is_owner".into(), false), + /// ("is_admin".into(), true) + /// ]) + /// )]); + /// # } + /// ``` + pub fn state(mut self, state: U) -> Self + where + U: Serialize + Send + Sync + 'static, + { + self.state = Some(state.serialize().ok()); + self + } + + /// A state that should be associated with the `user_id`. + /// + /// The presence event engine has already pre-processed `state` object, + /// which can be passed to the builder as is. + #[cfg(all(feature = "presence", feature = "std"))] + pub(crate) fn state_serialized(mut self, state: Option>) -> Self { + self.state = Some(state); + self + } +} + +#[allow(dead_code)] +impl HeartbeatRequestBuilder +where + T: Transport, + D: Deserializer + 'static, +{ + /// Build and call asynchronous request. + pub async fn execute(self) -> Result { + let request = self.request()?; + let transport_request = request.transport_request()?; + let client = request.pubnub_client.clone(); + let deserializer = client.deserializer.clone(); + transport_request + .send::(&client.transport, deserializer) + .await + } + + /// Build and call asynchronous request after delay. + /// + /// Perform delayed request call with ability to cancel it before call. + #[cfg(feature = "std")] + pub(in crate::dx::presence) async fn execute_with_cancel_and_delay( + self, + delay: Arc, + cancel_task: CancellationTask, + ) -> Result + where + F: Fn() -> BoxFuture<'static, ()> + Send + Sync + 'static, + { + select_biased! { + _ = cancel_task.wait_for_cancel().fuse() => { + Err(PubNubError::EffectCanceled) + }, + response = self.execute_with_delay(delay).fuse() => { + response + } + } + } + + /// Build and call asynchronous request after configured delay. + #[cfg(feature = "std")] + async fn execute_with_delay(self, delay: Arc) -> Result + where + F: Fn() -> BoxFuture<'static, ()> + Send + Sync + 'static, + { + // Postpone request execution. + delay().await; + + self.execute().await + } +} + +#[allow(dead_code)] +#[cfg(feature = "blocking")] +impl HeartbeatRequestBuilder +where + T: blocking::Transport, + D: Deserializer + 'static, +{ + /// Build and call synchronous request. + pub fn execute_blocking(self) -> Result { + let request = self.request()?; + let transport_request = request.transport_request()?; + let client = request.pubnub_client.clone(); + let deserializer = client.deserializer.clone(); + transport_request + .send_blocking::(&client.transport, deserializer) + } +} + +#[cfg(feature = "std")] +#[cfg(test)] +mod it_should { + use super::*; + use crate::{core::TransportResponse, PubNubClientBuilder}; + use futures::future::ready; + + #[tokio::test] + async fn be_able_to_cancel_delayed_heartbeat_call() { + struct MockTransport; + + #[async_trait::async_trait] + impl Transport for MockTransport { + async fn send(&self, _req: TransportRequest) -> Result { + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; // Simulate long request. + + Ok(TransportResponse::default()) + } + } + + let (tx, rx) = async_channel::bounded(1); + + let cancel_task = CancellationTask::new(rx, "test".into()); + + tx.send("test".into()).await.unwrap(); + + let result = PubNubClientBuilder::with_transport(MockTransport) + .with_keyset(crate::Keyset { + subscribe_key: "test", + publish_key: Some("test"), + secret_key: None, + }) + .with_user_id("test") + .build() + .unwrap() + .heartbeat() + .channels(vec!["test".into()]) + .execute_with_cancel_and_delay(Arc::new(|| ready(()).boxed()), cancel_task) + .await; + + assert!(matches!(result, Err(PubNubError::EffectCanceled))); + } +} diff --git a/src/dx/presence/builders/leave.rs b/src/dx/presence/builders/leave.rs new file mode 100644 index 00000000..642ca698 --- /dev/null +++ b/src/dx/presence/builders/leave.rs @@ -0,0 +1,161 @@ +//! # PubNub presence leave module. +//! +//! The [`LeaveRequestBuilder`] lets you make and execute requests that will +//! announce `leave` of `user_id` from provided channels and groups. + +use derive_builder::Builder; + +use crate::{ + core::{ + utils::{ + encoding::{url_encoded_channel_groups, url_encoded_channels}, + headers::{APPLICATION_JSON, CONTENT_TYPE}, + }, + Deserializer, PubNubError, Transport, TransportMethod, TransportRequest, + }, + dx::{ + presence::{ + builders, + result::{LeaveResponseBody, LeaveResult}, + }, + pubnub_client::PubNubClientInstance, + }, + lib::{ + alloc::{ + string::{String, ToString}, + vec, + }, + collections::HashMap, + }, +}; + +/// The [`LeaveRequestBuilder`] is used to build user `leave` announcement +/// request that is sent to the [`PubNub`] network. +/// +/// This struct is used by the [`leave`] method of the [`PubNubClient`]. +/// The [`leave`] method is used to announce specified `user_id` is leaving +/// provided channels and groups. +/// +/// [`PubNub`]:https://www.pubnub.com/ +#[derive(Builder)] +#[builder( + pattern = "owned", + build_fn(vis = "pub(in crate::dx::presence)", validate = "Self::validate"), + no_std +)] +pub struct LeaveRequest { + /// Current client which can provide transportation to perform the request. + /// + /// This field is used to get [`Transport`] to perform the request. + #[builder(field(vis = "pub(in crate::dx::presence)"), setter(custom))] + pub(in crate::dx::presence) pubnub_client: PubNubClientInstance, + + /// Channels for announcement. + #[builder( + field(vis = "pub(in crate::dx::presence)"), + setter(strip_option, into), + default = "vec![]" + )] + pub(in crate::dx::presence) channels: Vec, + + /// Channel groups for announcement. + #[builder( + field(vis = "pub(in crate::dx::presence)"), + setter(into, strip_option), + default = "vec![]" + )] + pub(in crate::dx::presence) channel_groups: Vec, + + #[builder(field(vis = "pub(in crate::dx::presence)"), setter(strip_option, into))] + /// Identifier for which `leave` in channels and/or channel groups will be + /// announced. + pub(in crate::dx::presence) user_id: String, +} + +impl LeaveRequestBuilder { + /// Validate user-provided data for request builder. + /// + /// Validator ensure that list of provided data is enough to build valid + /// presence leave request instance. + fn validate(&self) -> Result<(), String> { + let groups_len = self.channel_groups.as_ref().map_or_else(|| 0, |v| v.len()); + let channels_len = self.channels.as_ref().map_or_else(|| 0, |v| v.len()); + + builders::validate_configuration(&self.pubnub_client).and_then(|_| { + if channels_len == groups_len && channels_len == 0 { + Err("Either channels or channel groups should be provided".into()) + } else if self.user_id.is_none() { + Err("User id is missing".into()) + } else { + Ok(()) + } + }) + } + + /// Build [`LeaveRequest`] from builder. + fn request(self) -> Result, PubNubError> { + self.build() + .map_err(|err| PubNubError::general_api_error(err.to_string(), None, None)) + } +} + +impl LeaveRequest { + /// Create transport request from the request builder. + pub(in crate::dx::presence) fn transport_request( + &self, + ) -> Result { + let sub_key = &self.pubnub_client.config.subscribe_key; + let mut query: HashMap = HashMap::new(); + query.insert("uuid".into(), self.user_id.to_string()); + + // Serialize list of channel groups and add into query parameters list. + url_encoded_channel_groups(&self.channel_groups) + .and_then(|channel_groups| query.insert("channel-group".into(), channel_groups)); + + Ok(TransportRequest { + path: format!( + "/v2/presence/sub_key/{sub_key}/channel/{}/leave", + url_encoded_channels(&self.channels) + ), + query_parameters: query, + method: TransportMethod::Get, + headers: [(CONTENT_TYPE.into(), APPLICATION_JSON.into())].into(), + body: None, + }) + } +} + +impl LeaveRequestBuilder +where + T: Transport, + D: Deserializer + 'static, +{ + /// Build and call asynchronous request. + pub async fn execute(self) -> Result { + let request = self.request()?; + let transport_request = request.transport_request()?; + let client = request.pubnub_client.clone(); + let deserializer = client.deserializer.clone(); + transport_request + .send::(&client.transport, deserializer) + .await + } +} + +#[allow(dead_code)] +#[cfg(feature = "blocking")] +impl LeaveRequestBuilder +where + T: crate::core::blocking::Transport, + D: Deserializer + 'static, +{ + /// Build and call synchronous request. + pub fn execute_blocking(self) -> Result { + let request = self.request()?; + let transport_request = request.transport_request()?; + let client = request.pubnub_client.clone(); + let deserializer = client.deserializer.clone(); + transport_request + .send_blocking::(&client.transport, deserializer) + } +} diff --git a/src/dx/presence/builders/mod.rs b/src/dx/presence/builders/mod.rs new file mode 100644 index 00000000..6951d9c1 --- /dev/null +++ b/src/dx/presence/builders/mod.rs @@ -0,0 +1,38 @@ +//! # Presence API builders module. +//! +//! Module contains set fo builders which provide access to [`PubNub`] presence +//! API: [`SetStateRequestBuilder`]. +//! +//! [`PubNub`]: https://www.pubnub.com + +#[doc(inline)] +pub(crate) use heartbeat::HeartbeatRequestBuilder; +pub(crate) mod heartbeat; + +#[doc(inline)] +pub use set_state::{SetStateRequest, SetStateRequestBuilder}; +pub mod set_state; + +#[doc(inline)] +pub(crate) use leave::LeaveRequestBuilder; +pub(crate) mod leave; + +use crate::{dx::pubnub_client::PubNubClientInstance, lib::alloc::string::String}; + +/// Validate [`PubNubClient`] configuration. +/// +/// Check whether if the [`PubNubConfig`] contains all the required fields set +/// for presence endpoint usage or not. +pub(in crate::dx::presence::builders) fn validate_configuration( + client: &Option>, +) -> Result<(), String> { + let client = client + .as_ref() + .unwrap_or_else(|| panic!("PubNub client instance not set.")); + + if client.config.subscribe_key.is_empty() { + return Err("Incomplete PubNub client configuration: 'subscribe_key' is empty.".into()); + } + + Ok(()) +} diff --git a/src/dx/presence/builders/set_state.rs b/src/dx/presence/builders/set_state.rs new file mode 100644 index 00000000..a10d0f02 --- /dev/null +++ b/src/dx/presence/builders/set_state.rs @@ -0,0 +1,199 @@ +//! # PubNub set state module. +//! +//! The [`SetStateRequestBuilder`] lets you make and execute requests that will +//! associate the provided `state` with `user_id` on the provided list of +//! channels and channels in channel groups. + +use derive_builder::Builder; + +use crate::{ + core::{ + utils::{ + encoding::{ + url_encode_extended, url_encoded_channel_groups, url_encoded_channels, + UrlEncodeExtension, + }, + headers::{APPLICATION_JSON, CONTENT_TYPE}, + }, + Deserializer, PubNubError, Transport, TransportMethod, TransportRequest, + }, + dx::{ + presence::{ + builders, + result::{SetStateResponseBody, SetStateResult}, + }, + pubnub_client::PubNubClientInstance, + }, + lib::{ + alloc::{ + string::{String, ToString}, + vec, + }, + collections::HashMap, + }, +}; + +/// The [`SetStateRequestBuilder`] is used to build `user_id` associated state +/// update request that is sent to the [`PubNub`] network. +/// +/// This struct is used by the [`set_state`] method of the [`PubNubClient`]. +/// The [`set_state`] method is used to update state associated with `user_id` +/// on the provided channels and groups. +/// +/// [`PubNub`]:https://www.pubnub.com/ +#[derive(Builder)] +#[builder( + pattern = "owned", + build_fn(vis = "pub(in crate::dx::presence)", validate = "Self::validate"), + no_std +)] +pub struct SetStateRequest { + /// Current client which can provide transportation to perform the request. + /// + /// This field is used to get [`Transport`] to perform the request. + #[builder(field(vis = "pub(in crate::dx::presence)"), setter(custom))] + pub(in crate::dx::presence) pubnub_client: PubNubClientInstance, + + /// Channels with which state will be associated. + #[builder( + field(vis = "pub(in crate::dx::presence)"), + setter(strip_option, into), + default = "vec![]" + )] + pub(in crate::dx::presence) channels: Vec, + + /// Channel groups with which state will be associated. + /// + /// The specified state will be associated with channels that have been + /// included in the specified target channel groups. + #[builder( + field(vis = "pub(in crate::dx::presence)"), + setter(into, strip_option), + default = "vec![]" + )] + pub(in crate::dx::presence) channel_groups: Vec, + + /// A state that should be associated with the `user_id`. + /// + /// The `state` object should be a `HashMap` which represents information + /// that should be associated with `user_id`. + /// + /// # Example: + /// ```rust,no_run + /// # use std::collections::HashMap; + /// # fn main() { + /// let state = HashMap::::from([ + /// ("is_owner".into(), false), + /// ("is_admin".into(), true) + /// ]); + /// # } + /// ``` + #[builder( + field(vis = "pub(in crate::dx::presence)"), + setter(custom, strip_option) + )] + pub(in crate::dx::presence) state: Option>, + + #[builder(field(vis = "pub(in crate::dx::presence)"), setter(strip_option, into))] + /// Identifier for which `state` should be associated for provided list of + /// channels and groups. + pub(in crate::dx::presence) user_id: String, +} + +impl SetStateRequestBuilder { + /// Validate user-provided data for request builder. + /// + /// Validator ensure that list of provided data is enough to build valid + /// set state request instance. + fn validate(&self) -> Result<(), String> { + let groups_len = self.channel_groups.as_ref().map_or_else(|| 0, |v| v.len()); + let channels_len = self.channels.as_ref().map_or_else(|| 0, |v| v.len()); + + builders::validate_configuration(&self.pubnub_client).and_then(|_| { + if channels_len == groups_len && channels_len == 0 { + Err("Either channels or channel groups should be provided".into()) + } else if self.user_id.is_none() { + Err("User id is missing".into()) + } else if self.state.is_none() { + Err("State is missing".into()) + } else { + Ok(()) + } + }) + } + + /// Build [`SetStateRequest`] from builder. + fn request(self) -> Result, PubNubError> { + self.build() + .map_err(|err| PubNubError::general_api_error(err.to_string(), None, None)) + } +} + +impl SetStateRequest { + /// Create transport request from the request builder. + pub(in crate::dx::presence) fn transport_request( + &self, + ) -> Result { + let sub_key = &self.pubnub_client.config.subscribe_key; + let mut query: HashMap = HashMap::new(); + + // Serialize list of channel groups and add into query parameters list. + url_encoded_channel_groups(&self.channel_groups) + .and_then(|channel_groups| query.insert("channel-group".into(), channel_groups)); + + if let Some(state) = self.state.as_ref() { + let serialized_state = + String::from_utf8(state.clone()).map_err(|err| PubNubError::Serialization { + details: err.to_string(), + })?; + query.insert("state".into(), serialized_state); + } + + Ok(TransportRequest { + path: format!( + "/v2/presence/sub-key/{sub_key}/channel/{}/uuid/{}/data", + url_encoded_channels(&self.channels), + url_encode_extended(self.user_id.as_bytes(), UrlEncodeExtension::NonChannelPath) + ), + query_parameters: query, + method: TransportMethod::Get, + headers: [(CONTENT_TYPE.into(), APPLICATION_JSON.into())].into(), + body: None, + }) + } +} + +impl SetStateRequestBuilder +where + T: Transport, + D: Deserializer + 'static, +{ + /// Build and call asynchronous request. + pub async fn execute(self) -> Result { + let request = self.request()?; + let transport_request = request.transport_request()?; + let client = request.pubnub_client.clone(); + let deserializer = client.deserializer.clone(); + transport_request + .send::(&client.transport, deserializer) + .await + } +} + +#[allow(dead_code)] +#[cfg(feature = "blocking")] +impl SetStateRequestBuilder +where + T: crate::core::blocking::Transport, + D: Deserializer + 'static, +{ + /// Build and call synchronous request. + pub fn execute_blocking(self) -> Result { + let request = self.request()?; + let transport_request = request.transport_request()?; + let client = request.pubnub_client.clone(); + let deserializer = client.deserializer.clone(); + transport_request + .send_blocking::(&client.transport, deserializer) + } +} diff --git a/src/dx/presence/event_engine/effect_handler.rs b/src/dx/presence/event_engine/effect_handler.rs new file mode 100644 index 00000000..e0b10a09 --- /dev/null +++ b/src/dx/presence/event_engine/effect_handler.rs @@ -0,0 +1,102 @@ +//! # Presence event engine effects handler. +//! +//! The module contains the [`PresenceEffectHandler`] type, which is used by +//! event engine for + +use async_channel::Sender; + +use crate::{ + core::{event_engine::EffectHandler, RequestRetryPolicy}, + lib::{ + alloc::sync::Arc, + core::fmt::{Debug, Formatter, Result}, + }, + presence::event_engine::{ + effects::{HeartbeatEffectExecutor, LeaveEffectExecutor, WaitEffectExecutor}, + PresenceEffect, PresenceEffectInvocation, + }, +}; + +/// Presence effect handler. +/// +/// Handler responsible for effects implementation and creation in response on +/// effect invocation. +pub(crate) struct PresenceEffectHandler { + /// Heartbeat call function pointer. + heartbeat_call: Arc, + + /// Delayed heartbeat call function pointer. + delayed_heartbeat_call: Arc, + + /// Leave function pointer. + leave_call: Arc, + + /// Heartbeat interval wait function pointer. + wait_call: Arc, + + /// Retry policy. + retry_policy: RequestRetryPolicy, + + /// Cancellation channel. + cancellation_channel: Sender, +} + +impl PresenceEffectHandler { + /// Create presence effect handler. + pub fn new( + heartbeat_call: Arc, + delayed_heartbeat_call: Arc, + leave_call: Arc, + wait_call: Arc, + retry_policy: RequestRetryPolicy, + cancellation_channel: Sender, + ) -> Self { + Self { + heartbeat_call, + delayed_heartbeat_call, + leave_call, + wait_call, + retry_policy, + cancellation_channel, + } + } +} + +impl EffectHandler for PresenceEffectHandler { + fn create(&self, invocation: &PresenceEffectInvocation) -> Option { + match invocation { + PresenceEffectInvocation::Heartbeat { input } => Some(PresenceEffect::Heartbeat { + input: input.clone(), + executor: self.heartbeat_call.clone(), + }), + PresenceEffectInvocation::DelayedHeartbeat { + input, + attempts, + reason, + } => Some(PresenceEffect::DelayedHeartbeat { + input: input.clone(), + attempts: *attempts, + reason: reason.clone(), + retry_policy: self.retry_policy.clone(), + executor: self.delayed_heartbeat_call.clone(), + cancellation_channel: self.cancellation_channel.clone(), + }), + PresenceEffectInvocation::Leave { input } => Some(PresenceEffect::Leave { + input: input.clone(), + executor: self.leave_call.clone(), + }), + PresenceEffectInvocation::Wait { input } => Some(PresenceEffect::Wait { + input: input.clone(), + executor: self.wait_call.clone(), + cancellation_channel: self.cancellation_channel.clone(), + }), + _ => None, + } + } +} + +impl Debug for PresenceEffectHandler { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!(f, "PresenceEffectHandler {{}}") + } +} diff --git a/src/dx/presence/event_engine/effects/heartbeat.rs b/src/dx/presence/event_engine/effects/heartbeat.rs new file mode 100644 index 00000000..dbc9943b --- /dev/null +++ b/src/dx/presence/event_engine/effects/heartbeat.rs @@ -0,0 +1,185 @@ +//! Heartbeat effect module. +//! +//! Module contains implementation of `Heartbeat` and `Delayed heartbeat` effect +//! which is used to announce `user_id` presence on specified channels and +//! groups. + +use crate::{ + core::{PubNubError, RequestRetryPolicy}, + lib::alloc::{sync::Arc, vec, vec::Vec}, + presence::event_engine::{ + effects::HeartbeatEffectExecutor, + types::{PresenceInput, PresenceParameters}, + PresenceEvent, + }, +}; + +use futures::TryFutureExt; +use log::info; + +#[allow(clippy::too_many_arguments)] +pub(super) async fn execute( + input: &PresenceInput, + attempt: u8, + reason: Option, + effect_id: &str, + retry_policy: &Option, + executor: &Arc, +) -> Vec { + if let Some(retry_policy) = retry_policy { + match reason { + Some(reason) if !retry_policy.retriable(&attempt, Some(&reason)) => { + return vec![PresenceEvent::HeartbeatGiveUp { reason }]; + } + _ => {} + } + } + + let channel_groups: Option> = input.channel_groups(); + let channels: Option> = input.channels(); + + info!( + "Heartbeat for\nchannels: {:?}\nchannel groups: {:?}", + channels, channel_groups + ); + + executor(PresenceParameters { + channels: &channels, + channel_groups: &channel_groups, + attempt, + reason, + effect_id, + }) + .map_ok_or_else( + |error| vec![PresenceEvent::HeartbeatFailure { reason: error }], + |_| vec![PresenceEvent::HeartbeatSuccess], + ) + .await +} + +#[cfg(test)] +mod it_should { + use super::*; + use crate::{ + core::{PubNubError, TransportResponse}, + dx::presence::HeartbeatResult, + }; + use futures::FutureExt; + + #[tokio::test] + async fn return_heartbeat_success_event() { + let mocked_heartbeat_function: Arc = Arc::new(move |parameters| { + assert_eq!(parameters.channel_groups, &Some(vec!["cg1".to_string()])); + assert_eq!(parameters.channels, &Some(vec!["ch1".to_string()])); + assert_eq!(parameters.attempt, 0); + assert_eq!(parameters.reason, None); + assert_eq!(parameters.effect_id, "id"); + + async move { Ok(HeartbeatResult) }.boxed() + }); + + let result = execute( + &PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["cg1".to_string()]), + ), + 0, + None, + "id", + &Some(RequestRetryPolicy::None), + &mocked_heartbeat_function, + ) + .await; + + assert!(!result.is_empty()); + assert!(matches!( + result.first().unwrap(), + PresenceEvent::HeartbeatSuccess + )); + } + + #[tokio::test] + async fn return_heartbeat_failed_event_on_error() { + let mocked_heartbeat_function: Arc = Arc::new(move |_| { + async move { + Err(PubNubError::Transport { + details: "test".into(), + response: Some(Box::new(TransportResponse { + status: 500, + ..Default::default() + })), + }) + } + .boxed() + }); + + let result = execute( + &PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["cg1".to_string()]), + ), + 0, + Some(PubNubError::Transport { + details: "test".into(), + response: Some(Box::new(TransportResponse { + status: 500, + ..Default::default() + })), + }), + "id", + &Some(RequestRetryPolicy::None), + &mocked_heartbeat_function, + ) + .await; + + assert!(!result.is_empty()); + assert!(matches!( + result.first().unwrap(), + PresenceEvent::HeartbeatFailure { .. } + )); + } + + #[tokio::test] + async fn return_heartbeat_give_up_event_on_error() { + let mocked_heartbeat_function: Arc = Arc::new(move |_| { + async move { + Err(PubNubError::Transport { + details: "test".into(), + response: Some(Box::new(TransportResponse { + status: 500, + ..Default::default() + })), + }) + } + .boxed() + }); + + let result = execute( + &PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["cg1".to_string()]), + ), + 5, + Some(PubNubError::Transport { + details: "test".into(), + response: Some(Box::new(TransportResponse { + status: 500, + ..Default::default() + })), + }), + "id", + &Some(RequestRetryPolicy::Linear { + delay: 0, + max_retry: 1, + }), + &mocked_heartbeat_function, + ) + .await; + + assert!(!result.is_empty()); + assert!(matches!( + result.first().unwrap(), + PresenceEvent::HeartbeatGiveUp { .. } + )); + } +} diff --git a/src/dx/presence/event_engine/effects/leave.rs b/src/dx/presence/event_engine/effects/leave.rs new file mode 100644 index 00000000..55345322 --- /dev/null +++ b/src/dx/presence/event_engine/effects/leave.rs @@ -0,0 +1,105 @@ +//! Presence leave module. +//! +//! Module contains implementation of `Leave` effect which is used to announce +//! `user_id` `leave` from specified channels and groups. + +use log::info; + +use crate::{ + lib::alloc::{sync::Arc, vec, vec::Vec}, + presence::event_engine::{ + effects::LeaveEffectExecutor, + types::{PresenceInput, PresenceParameters}, + PresenceEvent, + }, +}; + +#[allow(clippy::too_many_arguments, dead_code)] +pub(super) async fn execute( + input: &PresenceInput, + effect_id: &str, + executor: &Arc, +) -> Vec { + let channel_groups = input.channel_groups(); + let channels = input.channels(); + + info!( + "Leave\nchannels: {:?}\nchannel groups: {:?}", + channels, channel_groups + ); + + let _ = executor(PresenceParameters { + channels: &channels, + channel_groups: &channel_groups, + attempt: 0, + reason: None, + effect_id, + }) + .await; + + vec![] +} + +#[cfg(test)] +mod it_should { + use futures::FutureExt; + + use super::*; + use crate::{ + core::{PubNubError, TransportResponse}, + presence::LeaveResult, + }; + + #[tokio::test] + async fn return_leave_success_event() { + let mocked_leave_function: Arc = Arc::new(move |parameters| { + assert_eq!(parameters.channel_groups, &Some(vec!["cg2".to_string()])); + assert_eq!(parameters.channels, &Some(vec!["ch2".to_string()])); + assert_eq!(parameters.attempt, 0); + assert_eq!(parameters.reason, None); + assert_eq!(parameters.effect_id, "id"); + + async move { Ok(LeaveResult) }.boxed() + }); + + let result = execute( + &PresenceInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["cg2".to_string()]), + ), + "id", + &mocked_leave_function, + ) + .await; + + assert!(result.is_empty()); + } + + #[tokio::test] + async fn return_leave_failed_event_on_error() { + let mocked_leave_function: Arc = Arc::new(move |_| { + async move { + Err(PubNubError::Transport { + details: "test".into(), + response: Some(Box::new(TransportResponse { + status: 500, + ..Default::default() + })), + }) + } + .boxed() + }); + + let result = execute( + &PresenceInput::new( + &Some(vec!["ch3".to_string()]), + &Some(vec!["cg3".to_string()]), + ), + "id", + &mocked_leave_function, + ) + .await; + + assert!(result.is_empty()); + } +} diff --git a/src/dx/presence/event_engine/effects/mod.rs b/src/dx/presence/event_engine/effects/mod.rs new file mode 100644 index 00000000..eeb60f79 --- /dev/null +++ b/src/dx/presence/event_engine/effects/mod.rs @@ -0,0 +1,249 @@ +//! # Presence event engine effect module. + +use async_channel::Sender; +use futures::future::BoxFuture; + +use crate::{ + core::{ + event_engine::{Effect, EffectInvocation}, + PubNubError, RequestRetryPolicy, + }, + lib::{ + alloc::{string::String, sync::Arc, vec::Vec}, + core::fmt::{Debug, Formatter}, + }, + presence::{ + event_engine::{ + types::{PresenceInput, PresenceParameters}, + PresenceEffectInvocation, + }, + HeartbeatResult, LeaveResult, + }, +}; + +mod heartbeat; +mod leave; +mod wait; + +/// Heartbeat effect executor. +/// +/// The provided closure should pass [`PresenceParameters`] to the heartbeat +/// [`PubNub API`] endpoint and return processed results. +/// +/// [`PubNub API`]: https://www.pubnub.com/docs +pub(in crate::dx::presence) type HeartbeatEffectExecutor = dyn Fn(PresenceParameters) -> BoxFuture<'static, Result> + + Send + + Sync; + +/// Wait effect executor. +/// +/// The provided closure should provide the ability to wait a specified amount +/// of time before further program execution. +pub(in crate::dx::presence) type WaitEffectExecutor = + dyn Fn(&str) -> BoxFuture<'static, Result<(), PubNubError>> + Send + Sync; + +/// Leave effect executor. +/// +/// The provided closure should pass [`PresenceParameters`] to the presence +/// leave [`PubNub API`] endpoint and return processed results. +/// +/// [`PubNub API`]: https://www.pubnub.com/docs +pub(in crate::dx::presence) type LeaveEffectExecutor = dyn Fn(PresenceParameters) -> BoxFuture<'static, Result> + + Send + + Sync; + +/// Presence state machine effects. +#[allow(dead_code)] +pub(crate) enum PresenceEffect { + /// Heartbeat effect invocation. + Heartbeat { + /// User input with channels and groups. + /// + /// Object contains list of channels and groups for which `user_id` + /// presence should be announced. + input: PresenceInput, + + /// Executor function. + /// + /// Function which will be used to execute heartbeat. + executor: Arc, + }, + + /// Delayed heartbeat effect invocation. + DelayedHeartbeat { + /// User input with channels and groups. + /// + /// Object contains list of channels and groups for which `user_id` + /// presence should be announced. + input: PresenceInput, + + /// Current heartbeat retry attempt. + /// + /// Used to track overall number of heartbeat retry attempts. + attempts: u8, + + /// Heartbeat attempt failure reason. + reason: PubNubError, + + /// Retry policy. + retry_policy: RequestRetryPolicy, + + /// Executor function. + /// + /// Function which will be used to execute heartbeat. + executor: Arc, + + /// Cancellation channel. + /// + /// Channel which will be used to cancel effect execution. + cancellation_channel: Sender, + }, + + /// Leave effect invocation. + Leave { + /// User input with channels and groups. + /// + /// Object contains list of channels and groups for which `user_id` + /// should leave. + input: PresenceInput, + + /// Executor function. + /// + /// Function which will be used to execute leave. + executor: Arc, + }, + + /// Delay effect invocation. + Wait { + /// User input with channels and groups. + /// + /// Object contains list of channels and groups for which `user_id` + /// presence should be announced after delay. + input: PresenceInput, + + /// Cancellation channel. + /// + /// Channel which will be used to cancel effect execution. + cancellation_channel: Sender, + + /// Executor function. + /// + /// Function which will be used to execute wait. + executor: Arc, + }, +} + +impl Debug for PresenceEffect { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Heartbeat { input, .. } => write!( + f, + "PresenceEffect::Heartbeat {{ channels: {:?}, channel groups: \ + {:?}}}", + input.channels(), + input.channel_groups() + ), + Self::DelayedHeartbeat { input, .. } => write!( + f, + "PresenceEffect::DelayedHeartbeat {{ channels: {:?}, channel groups: \ + {:?}}}", + input.channels(), + input.channel_groups() + ), + Self::Leave { input, .. } => write!( + f, + "PresenceEffect::Leave {{ channels: {:?}, channel groups: \ + {:?}}}", + input.channels(), + input.channel_groups() + ), + Self::Wait { input, .. } => write!( + f, + "PresenceEffect::Wait {{ channels: {:?}, channel groups: \ + {:?}}}", + input.channels(), + input.channel_groups() + ), + } + } +} + +#[async_trait::async_trait] +impl Effect for PresenceEffect { + type Invocation = PresenceEffectInvocation; + + fn id(&self) -> String { + match self { + Self::Heartbeat { .. } => "HEARTBEAT", + Self::DelayedHeartbeat { .. } => "DELAYED_HEARTBEAT", + Self::Leave { .. } => "LEAVE", + Self::Wait { .. } => "WAIT", + } + .into() + } + + async fn run(&self) -> Vec<::Event> { + match self { + Self::Heartbeat { input, executor } => { + heartbeat::execute(input, 0, None, &self.id(), &None, executor).await + } + Self::DelayedHeartbeat { + input, + attempts, + reason, + retry_policy, + executor, + .. + } => { + heartbeat::execute( + input, + *attempts, + Some(reason.clone()), + &self.id(), + &Some(retry_policy.clone()), + executor, + ) + .await + } + Self::Leave { input, executor } => leave::execute(input, &self.id(), executor).await, + Self::Wait { executor, .. } => wait::execute(&self.id(), executor).await, + } + } + + fn cancel(&self) { + match self { + PresenceEffect::DelayedHeartbeat { + cancellation_channel, + .. + } + | PresenceEffect::Wait { + cancellation_channel, + .. + } => { + cancellation_channel + .send_blocking(self.id()) + .expect("Cancellation pipe is broken!"); + } + _ => { /* cannot cancel other effects */ } + } + } +} + +#[cfg(test)] +mod should { + use super::*; + + #[tokio::test] + async fn send_cancellation_notification() { + let (tx, rx) = async_channel::bounded(1); + + let effect = PresenceEffect::Wait { + input: PresenceInput::new(&None, &None), + executor: Arc::new(|_| Box::pin(async move { Ok(()) })), + cancellation_channel: tx, + }; + + effect.cancel(); + assert_eq!(rx.recv().await.unwrap(), effect.id()) + } +} diff --git a/src/dx/presence/event_engine/effects/wait.rs b/src/dx/presence/event_engine/effects/wait.rs new file mode 100644 index 00000000..984a70f9 --- /dev/null +++ b/src/dx/presence/event_engine/effects/wait.rs @@ -0,0 +1,52 @@ +//! Module with `Wait` effect implementation. + +use crate::{ + lib::alloc::{sync::Arc, vec, vec::Vec}, + presence::event_engine::{effects::WaitEffectExecutor, PresenceEvent}, +}; + +use futures::TryFutureExt; +use log::info; + +pub(super) async fn execute( + effect_id: &str, + executor: &Arc, +) -> Vec { + info!("Heartbeat cooling down"); + + executor(effect_id) + .map_ok_or_else(|_| vec![], |_| vec![PresenceEvent::TimesUp]) + .await +} + +#[cfg(test)] +mod it_should { + use super::*; + use crate::core::PubNubError; + use futures::{future::ready, FutureExt}; + + #[tokio::test] + async fn return_time_up_event() { + let mock_wait_function: Arc = Arc::new(move |effect_id| { + assert_eq!(effect_id, "id"); + ready(Ok(())).boxed() + }); + + let result = execute("id", &mock_wait_function).await; + + assert!(!result.is_empty()); + assert!(matches!(result.first().unwrap(), PresenceEvent::TimesUp)); + } + + #[tokio::test] + async fn return_empty_list_on_cancel() { + let mock_wait_function: Arc = Arc::new(move |effect_id| { + assert_eq!(effect_id, "id"); + ready(Err(PubNubError::EffectCanceled)).boxed() + }); + + let result = execute("id", &mock_wait_function).await; + + assert!(result.is_empty()); + } +} diff --git a/src/dx/presence/event_engine/event.rs b/src/dx/presence/event_engine/event.rs new file mode 100644 index 00000000..5e50fb1e --- /dev/null +++ b/src/dx/presence/event_engine/event.rs @@ -0,0 +1,107 @@ +//! Heartbeat Event Engine event module. +//! +//! The module contains the [`PresenceEvent`] type, which describes available +//! event engine transition events. + +use crate::core::{event_engine::Event, PubNubError}; + +#[derive(Debug)] +pub(crate) enum PresenceEvent { + /// Announce join to channels and groups. + /// + /// Announce `user_id` presence on new channels and groups. + #[allow(dead_code)] + Joined { + /// Optional list of channels. + /// + /// List of channels for which `user_id` presence should be announced. + channels: Option>, + + /// Optional list of channel groups. + /// + /// List of channel groups for which `user_id` presence should be + /// announced. + channel_groups: Option>, + }, + + /// Announce leave on channels and groups. + /// + /// Announce `user_id` leave from channels and groups. + #[allow(dead_code)] + Left { + /// Optional list of channels. + /// + /// List of channels for which `user_id` should leave. + channels: Option>, + + /// Optional list of channel groups. + /// + /// List of channel groups for which `user_id` should leave. + channel_groups: Option>, + }, + + /// Announce leave on all channels and groups. + /// + /// Announce `user_id` leave from all channels and groups. + #[allow(dead_code)] + LeftAll, + + /// Heartbeat completed successfully. + /// + /// Emitted when [`PubNub`] network returned `OK` response. + #[allow(dead_code)] + HeartbeatSuccess, + + /// Heartbeat completed with an error. + /// + /// Emitted when another heartbeat effect attempt was unable to receive + /// response from [`PubNub`] network (network or permission issues). + /// + /// [`PubNub`]: https://www.pubnub.com/ + #[allow(dead_code)] + HeartbeatFailure { reason: PubNubError }, + + /// All heartbeat attempts was unsuccessful. + /// + /// Emitted when heartbeat attempts reached maximum allowed count (according + /// to retry / reconnection policy) and all following attempts should be + /// stopped. + #[allow(dead_code)] + HeartbeatGiveUp { reason: PubNubError }, + + /// Restore heartbeating. + /// + /// Re-launch heartbeat event engine. + #[allow(dead_code)] + Reconnect, + + /// Temporarily stop event engine. + /// + /// Suspend any delayed and waiting heartbeat endpoint calls till + /// `Reconnect` event will be triggered again. + #[allow(dead_code)] + Disconnect, + + /// Delay times up event. + /// + /// Emitted when `delay` reaches the end and should transit to the next + /// state. + #[allow(dead_code)] + TimesUp, +} + +impl Event for PresenceEvent { + fn id(&self) -> &str { + match self { + Self::Joined { .. } => "JOINED", + Self::Left { .. } => "LEFT", + Self::LeftAll => "LEFT_ALL", + Self::HeartbeatSuccess => "HEARTBEAT_SUCCESS", + Self::HeartbeatFailure { .. } => "HEARTBEAT_FAILED", + Self::HeartbeatGiveUp { .. } => "HEARTBEAT_GIVEUP", + Self::Reconnect => "RECONNECT", + Self::Disconnect => "DISCONNECT", + Self::TimesUp => "TIMES_UP", + } + } +} diff --git a/src/dx/presence/event_engine/invocation.rs b/src/dx/presence/event_engine/invocation.rs new file mode 100644 index 00000000..c81e7696 --- /dev/null +++ b/src/dx/presence/event_engine/invocation.rs @@ -0,0 +1,108 @@ +//! Heartbeat Event Engine effect invocation module. +//! +//! The module contains the [`PresenceEffectInvocation`] type, which describes +//! available event engine effect invocations. + +use crate::{ + core::{event_engine::EffectInvocation, PubNubError}, + lib::core::fmt::{Display, Formatter, Result}, + presence::event_engine::{PresenceEffect, PresenceEvent, PresenceInput}, +}; + +#[derive(Debug)] +#[allow(dead_code)] +pub(crate) enum PresenceEffectInvocation { + /// Heartbeat effect invocation. + Heartbeat { + /// User input with channels and groups. + /// + /// Object contains list of channels and groups for which `user_id` + /// presence should be announced. + input: PresenceInput, + }, + + /// Delayed heartbeat effect invocation. + DelayedHeartbeat { + /// User input with channels and groups. + /// + /// Object contains list of channels and groups for which `user_id` + /// presence should be announced. + input: PresenceInput, + + /// Delayed heartbeat retry attempt. + /// + /// Used to track overall number of delayed heartbeat retry attempts. + attempts: u8, + + /// Delayed heartbeat attempt failure reason. + reason: PubNubError, + }, + + /// Cancel delayed heartbeat effect invocation. + CancelDelayedHeartbeat, + + /// Leave effect invocation. + Leave { + /// User input with channels and groups. + /// + /// Object contains list of channels and groups for which `user_id` + /// should leave. + input: PresenceInput, + }, + + /// Delay effect invocation. + Wait { + /// User input with channels and groups. + /// + /// Object contains list of channels and groups for which `user_id` + /// presence should be announced. + input: PresenceInput, + }, + + /// Cancel delay effect invocation. + CancelWait, +} + +impl EffectInvocation for PresenceEffectInvocation { + type Effect = PresenceEffect; + type Event = PresenceEvent; + + fn id(&self) -> &str { + match self { + Self::Heartbeat { .. } => "HEARTBEAT", + Self::DelayedHeartbeat { .. } => "DELAYED_HEARTBEAT", + Self::CancelDelayedHeartbeat => "CANCEL_DELAYED_HEARTBEAT", + Self::Leave { .. } => "LEAVE", + Self::Wait { .. } => "WAIT", + Self::CancelWait => "CANCEL_WAIT", + } + } + + fn managed(&self) -> bool { + matches!(self, Self::Wait { .. } | Self::DelayedHeartbeat { .. }) + } + + fn cancelling(&self) -> bool { + matches!(self, Self::CancelDelayedHeartbeat | Self::CancelWait) + } + + fn cancelling_effect(&self, effect: &Self::Effect) -> bool { + (matches!(effect, PresenceEffect::DelayedHeartbeat { .. }) + && matches!(self, Self::CancelDelayedHeartbeat { .. })) + || (matches!(effect, PresenceEffect::Wait { .. }) + && matches!(self, Self::CancelWait { .. })) + } +} + +impl Display for PresenceEffectInvocation { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + match self { + Self::Heartbeat { .. } => write!(f, "HEARTBEAT"), + Self::DelayedHeartbeat { .. } => write!(f, "DELAYED_HEARTBEAT"), + Self::CancelDelayedHeartbeat => write!(f, "CANCEL_DELAYED_HEARTBEAT"), + Self::Leave { .. } => write!(f, "LEAVE"), + Self::Wait { .. } => write!(f, "WAIT"), + Self::CancelWait => write!(f, "CANCEL_WAIT"), + } + } +} diff --git a/src/dx/presence/event_engine/mod.rs b/src/dx/presence/event_engine/mod.rs new file mode 100644 index 00000000..498e77c6 --- /dev/null +++ b/src/dx/presence/event_engine/mod.rs @@ -0,0 +1,35 @@ +//! # Presence Event Engine module + +use crate::core::event_engine::EventEngine; + +#[doc(inline)] +#[allow(unused_imports)] +pub(crate) use effects::PresenceEffect; +pub(crate) mod effects; + +#[doc(inline)] +pub(crate) use effect_handler::PresenceEffectHandler; +pub(crate) mod effect_handler; + +#[doc(inline)] +#[allow(unused_imports)] +pub(crate) use invocation::PresenceEffectInvocation; +pub(crate) mod invocation; + +#[doc(inline)] +#[allow(unused_imports)] +pub(crate) use event::PresenceEvent; +pub(crate) mod event; + +#[doc(inline)] +#[allow(unused_imports)] +pub(crate) use state::PresenceState; +pub(crate) mod state; + +#[doc(inline)] +#[allow(unused_imports)] +pub(in crate::dx::presence) use types::{PresenceInput, PresenceParameters}; +pub(in crate::dx::presence) mod types; + +pub(crate) type PresenceEventEngine = + EventEngine; diff --git a/src/dx/presence/event_engine/state.rs b/src/dx/presence/event_engine/state.rs new file mode 100644 index 00000000..09d0d24a --- /dev/null +++ b/src/dx/presence/event_engine/state.rs @@ -0,0 +1,1374 @@ +//! # Heartbeat event engine state module. +//! +//! The module contains the [`PresenceState`] type, which describes available +//! event engine states. The module also contains an implementation of +//! `transition` between states in response to certain events. + +use crate::{ + core::{ + event_engine::{EffectInvocation, State, Transition}, + PubNubError, + }, + lib::alloc::string::String, + presence::event_engine::{ + PresenceEffectInvocation::{self, *}, + PresenceEvent, PresenceInput, + }, +}; + +/// Available `Heartbeat` event engine states. +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum PresenceState { + /// Inactive heartbeat state. + /// + /// The initial state jas no information about channels or groups for which + /// event engine should keep presence of `user_id`. + Inactive, + + /// Heartbeating state. + /// + /// Sending explicit heartbeat request for known channels and groups to the + /// [`PubNub`] network. + /// + /// [`PubNub`]:https://www.pubnub.com/ + Heartbeating { + /// User input with channels and groups. + /// + /// Object contains list of channels and groups for which `user_id` + /// presence should be announced. + input: PresenceInput, + }, + + /// Cooling down state. + /// + /// Heartbeating idle state in which it stay for configured amount of time. + Cooldown { + /// User input with channels and groups. + /// + /// Object contains list of channels and groups for which `user_id` + /// presence should be announced after configured heartbeat interval. + input: PresenceInput, + }, + + /// Heartbeat recovering state. + /// + /// The system is recovering after heartbeating attempt failure. + Reconnecting { + /// User input with channels and groups. + /// + /// Object contains list of channels and groups for which `user_id` + /// presence will be announced after heartbeating restore. + input: PresenceInput, + + /// Current heartbeating retry attempt. + /// + /// Used to track overall number of heartbeating retry attempts. + attempts: u8, + + /// Heartbeating attempt failure reason. + reason: PubNubError, + }, + + /// Heartbeat stopped state. + /// + /// Heartbeat explicitly has been stopped in response on user actions with + /// subscription. + Stopped { + /// User input with channels and groups. + /// + /// Object contains list of channels and groups for which `user_id` + /// presence will be announced after heartbeating restore. + input: PresenceInput, + }, + + /// Heartbeating failure state. + /// + /// System wasn't able to perform heartbeating after fixed number of + /// attempts. + Failed { + /// User input with channels and groups. + /// + /// Object contains list of channels and groups for which `user_id` + /// presence will be announced after heartbeating restore. + input: PresenceInput, + + /// Heartbeating attempt failure reason. + reason: PubNubError, + }, +} + +impl PresenceState { + /// Handle `joined` event. + fn presence_joined_transition( + &self, + channels: &Option>, + channel_groups: &Option>, + ) -> Option> { + let event_input = PresenceInput::new(channels, channel_groups); + + match self { + Self::Inactive => { + Some(self.transition_to(Self::Heartbeating { input: event_input }, None)) + } + Self::Heartbeating { input } + | Self::Cooldown { input } + | Self::Reconnecting { input, .. } + | Self::Failed { input, .. } + | Self::Stopped { input } + if &event_input != input => + { + let input = input.clone() + event_input; + Some(self.transition_to( + if !matches!(self, Self::Stopped { .. }) { + Self::Heartbeating { input } + } else { + Self::Stopped { input } + }, + None, + )) + } + _ => None, + } + } + + /// Handle `left` event. + fn presence_left_transition( + &self, + channels: &Option>, + channel_groups: &Option>, + ) -> Option> { + let event_input = PresenceInput::new(channels, channel_groups); + + match self { + Self::Inactive => Some(self.transition_to(Self::Stopped { input: event_input }, None)), + Self::Heartbeating { input } + | Self::Cooldown { input } + | Self::Reconnecting { input, .. } + | Self::Failed { input, .. } + | Self::Stopped { input } => { + let channels_for_heartbeating = input.clone() - event_input.clone(); + // Calculate actual list for which `Leave` invocation should be created. + let channels_to_leave = input.clone() - channels_for_heartbeating.clone(); + + (!channels_to_leave.is_empty).then(|| { + self.transition_to( + if !channels_for_heartbeating.is_empty { + if !matches!(self, Self::Stopped { .. }) { + Self::Heartbeating { + input: channels_for_heartbeating, + } + } else { + Self::Stopped { + input: channels_for_heartbeating, + } + } + } else { + Self::Inactive + }, + Some(vec![Leave { + input: channels_to_leave, + }]), + ) + }) + } + } + } + + /// Handle `left all` event. + fn presence_left_all_transition(&self) -> Option> { + match self { + Self::Heartbeating { input } + | Self::Cooldown { input } + | Self::Reconnecting { input, .. } + | Self::Failed { input, .. } => Some(self.transition_to( + Self::Inactive, + Some(vec![Leave { + input: input.clone(), + }]), + )), + Self::Stopped { .. } => Some(self.transition_to(Self::Inactive, None)), + _ => None, + } + } + + /// Handle `heartbeat success` event. + fn presence_heartbeat_success_transition( + &self, + ) -> Option> { + match self { + Self::Heartbeating { input } | Self::Reconnecting { input, .. } => { + Some(self.transition_to( + Self::Cooldown { + input: input.clone(), + }, + None, + )) + } + _ => None, + } + } + + /// Handle `heartbeat failure` event. + fn presence_heartbeat_failed_transition( + &self, + reason: &PubNubError, + ) -> Option> { + match self { + Self::Heartbeating { input } => Some(self.transition_to( + Self::Reconnecting { + input: input.clone(), + attempts: 1, + reason: reason.clone(), + }, + None, + )), + Self::Reconnecting { + input, attempts, .. + } => Some(self.transition_to( + Self::Reconnecting { + input: input.clone(), + attempts: attempts + 1, + reason: reason.clone(), + }, + None, + )), + _ => None, + } + } + + /// Handle `heartbeat give up` event. + fn presence_heartbeat_give_up_transition( + &self, + reason: &PubNubError, + ) -> Option> { + match self { + Self::Reconnecting { input, .. } => Some(self.transition_to( + Self::Failed { + input: input.clone(), + reason: reason.clone(), + }, + None, + )), + _ => None, + } + } + + /// Handle `reconnect` event. + fn presence_reconnect_transition(&self) -> Option> { + match self { + Self::Stopped { input } | Self::Failed { input, .. } => Some(self.transition_to( + Self::Heartbeating { + input: input.clone(), + }, + None, + )), + _ => None, + } + } + + /// Handle `reconnect` event. + fn presence_disconnect_transition(&self) -> Option> { + match self { + Self::Heartbeating { input } + | Self::Cooldown { input } + | Self::Reconnecting { input, .. } + | Self::Failed { input, .. } => Some(self.transition_to( + Self::Stopped { + input: input.clone(), + }, + Some(vec![Leave { + input: input.clone(), + }]), + )), + _ => None, + } + } + + /// Handle cooldown `times up` event. + fn presence_times_up_transition(&self) -> Option> { + match self { + Self::Cooldown { input } => Some(self.transition_to( + Self::Heartbeating { + input: input.clone(), + }, + None, + )), + _ => None, + } + } +} + +impl State for PresenceState { + type State = Self; + type Invocation = PresenceEffectInvocation; + type Event = PresenceEvent; + + fn enter(&self) -> Option> { + match self { + Self::Heartbeating { input } => Some(vec![Heartbeat { + input: input.clone(), + }]), + Self::Cooldown { input } => Some(vec![Wait { + input: input.clone(), + }]), + Self::Reconnecting { + input, + attempts, + reason, + } => Some(vec![DelayedHeartbeat { + input: input.clone(), + attempts: *attempts, + reason: reason.clone(), + }]), + _ => None, + } + } + + fn exit(&self) -> Option> { + match self { + PresenceState::Cooldown { .. } => Some(vec![CancelWait]), + PresenceState::Reconnecting { .. } => Some(vec![CancelDelayedHeartbeat]), + _ => None, + } + } + + fn transition( + &self, + event: &<::Invocation as EffectInvocation>::Event, + ) -> Option> { + match event { + PresenceEvent::Joined { + channels, + channel_groups, + } => self.presence_joined_transition(channels, channel_groups), + PresenceEvent::Left { + channels, + channel_groups, + } => self.presence_left_transition(channels, channel_groups), + PresenceEvent::LeftAll => self.presence_left_all_transition(), + PresenceEvent::HeartbeatSuccess => self.presence_heartbeat_success_transition(), + PresenceEvent::HeartbeatFailure { reason } => { + self.presence_heartbeat_failed_transition(reason) + } + PresenceEvent::HeartbeatGiveUp { reason } => { + self.presence_heartbeat_give_up_transition(reason) + } + PresenceEvent::Reconnect => self.presence_reconnect_transition(), + PresenceEvent::Disconnect => self.presence_disconnect_transition(), + PresenceEvent::TimesUp => self.presence_times_up_transition(), + } + } + + fn transition_to( + &self, + state: Self::State, + invocations: Option>, + ) -> Transition { + Transition { + invocations: self + .exit() + .unwrap_or(vec![]) + .into_iter() + .chain(invocations.unwrap_or(vec![])) + .chain(state.enter().unwrap_or(vec![])) + .collect(), + state, + } + } +} + +#[cfg(test)] +mod it_should { + use super::*; + use crate::presence::event_engine::effects::LeaveEffectExecutor; + use crate::presence::LeaveResult; + use crate::{ + core::{event_engine::EventEngine, RequestRetryPolicy}, + lib::alloc::sync::Arc, + presence::{ + event_engine::{ + effects::{HeartbeatEffectExecutor, WaitEffectExecutor}, + PresenceEffectHandler, PresenceEventEngine, + }, + HeartbeatResult, + }, + providers::futures_tokio::RuntimeTokio, + }; + use futures::FutureExt; + use test_case::test_case; + + fn event_engine(start_state: PresenceState) -> Arc { + let heartbeat_call: Arc = + Arc::new(|_| async move { Ok(HeartbeatResult) }.boxed()); + let delayed_heartbeat_call: Arc = + Arc::new(|_| async move { Ok(HeartbeatResult) }.boxed()); + let leave_call: Arc = + Arc::new(|_| async move { Ok(LeaveResult) }.boxed()); + let wait_call: Arc = Arc::new(|_| async move { Ok(()) }.boxed()); + + let (tx, _) = async_channel::bounded(1); + + EventEngine::new( + PresenceEffectHandler::new( + heartbeat_call, + delayed_heartbeat_call, + leave_call, + wait_call, + RequestRetryPolicy::None, + tx, + ), + start_state, + RuntimeTokio, + ) + } + + #[test_case( + PresenceState::Inactive, + PresenceEvent::Joined { + channels: Some(vec!["ch1".to_string()]), + channel_groups: Some(vec!["gr1".to_string()]), + }, + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }; + "to heartbeating on joined" + )] + #[test_case( + PresenceState::Inactive, + PresenceEvent::HeartbeatFailure { + reason: PubNubError::Transport { details: "Test".to_string(), response: None } + }, + PresenceState::Inactive; + "to not change on unexpected event" + )] + #[tokio::test] + async fn transition_for_inactive_state( + init_state: PresenceState, + event: PresenceEvent, + target_state: PresenceState, + ) { + let engine = event_engine(init_state.clone()); + assert!(matches!(init_state, PresenceState::Inactive)); + assert_eq!(engine.current_state(), init_state); + + // Process event. + engine.process(&event); + + assert_eq!(engine.current_state(), target_state); + } + + #[test_case( + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }, + PresenceEvent::Joined { + channels: Some(vec!["ch2".to_string()]), + channel_groups: Some(vec!["gr2".to_string()]), + }, + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string(), "ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ) + }; + "to heartbeating on joined" + )] + #[test_case( + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string(), "ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ) + }, + PresenceEvent::Left { + channels: None, + channel_groups: Some(vec!["gr1".to_string()]), + }, + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string(), "ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ) + }; + "to heartbeating on left" + )] + #[test_case( + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string(), "ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ) + }, + PresenceEvent::Left { + channels: Some(vec!["ch1".to_string(), "ch2".to_string()]), + channel_groups: Some(vec!["gr1".to_string(), "gr2".to_string()]), + }, + PresenceState::Inactive; + "to inactive on left for all channels and groups" + )] + #[test_case( + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }, + PresenceEvent::HeartbeatSuccess, + PresenceState::Cooldown { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }; + "to heartbeat cool down on heartbeat success" + )] + #[test_case( + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }, + PresenceEvent::HeartbeatFailure { + reason: PubNubError::Transport { details: "Test".to_string(), response: None } + }, + PresenceState::Reconnecting { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + attempts: 1, + reason: PubNubError::Transport { details: "Test".to_string(), response: None } + }; + "to reconnect on heartbeat failure" + )] + #[test_case( + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }, + PresenceEvent::Disconnect, + PresenceState::Stopped { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }; + "to stopped on disconnect" + )] + #[test_case( + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }, + PresenceEvent::LeftAll, + PresenceState::Inactive; + "to inactive on left all" + )] + #[test_case( + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }, + PresenceEvent::Joined { + channels: Some(vec!["ch1".to_string()]), + channel_groups: Some(vec!["gr1".to_string()]), + }, + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }; + "to not change on joined with same channels and groups" + )] + #[test_case( + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string(), "ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ) + }, + PresenceEvent::Left { + channels: None, + channel_groups: Some(vec!["gr3".to_string()]), + }, + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string(), "ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ) + }; + "to not change on left with unknown channels and groups" + )] + #[test_case( + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }, + PresenceEvent::HeartbeatGiveUp { + reason: PubNubError::Transport { details: "Test".to_string(), response: None } + }, + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }; + "to not change on unexpected event" + )] + #[tokio::test] + async fn transition_for_heartbeating_state( + init_state: PresenceState, + event: PresenceEvent, + target_state: PresenceState, + ) { + let engine = event_engine(init_state.clone()); + assert!(matches!(init_state, PresenceState::Heartbeating { .. })); + assert_eq!(engine.current_state(), init_state); + + // Process event. + engine.process(&event); + + assert_eq!(engine.current_state(), target_state); + } + + #[test_case( + PresenceState::Cooldown { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }, + PresenceEvent::Joined { + channels: Some(vec!["ch2".to_string()]), + channel_groups: Some(vec!["gr2".to_string()]), + }, + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string(), "ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ) + }; + "to heartbeating on joined" + )] + #[test_case( + PresenceState::Cooldown { + input: PresenceInput::new( + &Some(vec!["ch1".to_string(), "ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ) + }, + PresenceEvent::Left { + channels: Some(vec!["ch1".to_string()]), + channel_groups: None, + }, + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ) + }; + "to heartbeating on left" + )] + #[test_case( + PresenceState::Cooldown { + input: PresenceInput::new( + &Some(vec!["ch1".to_string(), "ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ) + }, + PresenceEvent::Left { + channels: Some(vec!["ch1".to_string(), "ch2".to_string()]), + channel_groups: Some(vec!["gr1".to_string(), "gr2".to_string()]), + }, + PresenceState::Inactive; + "to inactive on left for all channels and groups" + )] + #[test_case( + PresenceState::Cooldown { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }, + PresenceEvent::TimesUp, + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }; + "to heartbeating on times up" + )] + #[test_case( + PresenceState::Cooldown { + input: PresenceInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }, + PresenceEvent::Disconnect, + PresenceState::Stopped { + input: PresenceInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }; + "to stopped on disconnect" + )] + #[test_case( + PresenceState::Cooldown { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }, + PresenceEvent::LeftAll, + PresenceState::Inactive; + "to inactive on left all" + )] + #[test_case( + PresenceState::Cooldown { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }, + PresenceEvent::Joined { + channels: Some(vec!["ch1".to_string()]), + channel_groups: Some(vec!["gr1".to_string()]), + }, + PresenceState::Cooldown { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }; + "to not change on joined with same channels and groups" + )] + #[test_case( + PresenceState::Cooldown { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }, + PresenceEvent::Left { + channels: None, + channel_groups: Some(vec!["gr3".to_string()]), + }, + PresenceState::Cooldown { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }; + "to not change on left with unknown channels and groups" + )] + #[test_case( + PresenceState::Cooldown { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }, + PresenceEvent::HeartbeatGiveUp { + reason: PubNubError::Transport { details: "Test".to_string(), response: None } + }, + PresenceState::Cooldown { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }; + "to not change on unexpected event" + )] + #[tokio::test] + async fn transition_for_cool_down_state( + init_state: PresenceState, + event: PresenceEvent, + target_state: PresenceState, + ) { + let engine = event_engine(init_state.clone()); + assert!(matches!(init_state, PresenceState::Cooldown { .. })); + assert_eq!(engine.current_state(), init_state); + + // Process event. + engine.process(&event); + + assert_eq!(engine.current_state(), target_state); + } + + #[test_case( + PresenceState::Reconnecting { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + PresenceEvent::HeartbeatFailure { + reason: PubNubError::Transport { details: "Test reason on error".to_string(), response: None, }, + }, + PresenceState::Reconnecting { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + attempts: 2, + reason: PubNubError::Transport { details: "Test reason on error".to_string(), response: None, }, + }; + "to heartbeat reconnecting on heartbeat failure" + )] + #[test_case( + PresenceState::Reconnecting { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + PresenceEvent::Joined { + channels: Some(vec!["ch2".to_string()]), + channel_groups: Some(vec!["gr2".to_string()]), + }, + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string(), "ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ) + }; + "to heartbeating on joined" + )] + #[test_case( + PresenceState::Reconnecting { + input: PresenceInput::new( + &Some(vec!["ch1".to_string(), "ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ), + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + PresenceEvent::Left { + channels: Some(vec!["ch1".to_string()]), + channel_groups: None, + }, + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ) + }; + "to heartbeating on left" + )] + #[test_case( + PresenceState::Reconnecting { + input: PresenceInput::new( + &Some(vec!["ch1".to_string(), "ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ), + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + PresenceEvent::Left { + channels: Some(vec!["ch1".to_string(), "ch2".to_string()]), + channel_groups: Some(vec!["gr1".to_string(), "gr2".to_string()]), + }, + PresenceState::Inactive; + "to inactive on left for all channels and groups" + )] + #[test_case( + PresenceState::Reconnecting { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + PresenceEvent::HeartbeatSuccess, + PresenceState::Cooldown { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }; + "to cool down on heartbeat success" + )] + #[test_case( + PresenceState::Reconnecting { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + PresenceEvent::HeartbeatGiveUp { + reason: PubNubError::Transport { details: "Test reason on error".to_string(), response: None, }, + }, + PresenceState::Failed { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + reason: PubNubError::Transport { details: "Test reason on error".to_string(), response: None, }, + }; + "to failed on heartbeat give up" + )] + #[test_case( + PresenceState::Reconnecting { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + PresenceEvent::Disconnect, + PresenceState::Stopped { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }; + "to stopped on disconnect" + )] + #[test_case( + PresenceState::Reconnecting { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + PresenceEvent::LeftAll, + PresenceState::Inactive; + "to inactive on left all" + )] + #[test_case( + PresenceState::Reconnecting { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + PresenceEvent::Joined { + channels: Some(vec!["ch1".to_string()]), + channel_groups: Some(vec!["gr1".to_string()]), + }, + PresenceState::Reconnecting { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }; + "to not change on joined with same channels and groups" + )] + #[test_case( + PresenceState::Reconnecting { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + PresenceEvent::Left { + channels: None, + channel_groups: Some(vec!["gr3".to_string()]), + }, + PresenceState::Reconnecting { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }; + "to not change on left with unknown channels and groups" + )] + #[test_case( + PresenceState::Reconnecting { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + PresenceEvent::Reconnect, + PresenceState::Reconnecting { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + attempts: 1, + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }; + "to not change on unexpected event" + )] + #[tokio::test] + async fn transition_for_reconnecting_state( + init_state: PresenceState, + event: PresenceEvent, + target_state: PresenceState, + ) { + let engine = event_engine(init_state.clone()); + assert!(matches!(init_state, PresenceState::Reconnecting { .. })); + assert_eq!(engine.current_state(), init_state); + + // Process event. + engine.process(&event); + + assert_eq!(engine.current_state(), target_state); + } + + #[test_case( + PresenceState::Stopped { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }, + PresenceEvent::Joined { + channels: Some(vec!["ch2".to_string()]), + channel_groups: Some(vec!["gr2".to_string()]), + }, + PresenceState::Stopped { + input: PresenceInput::new( + &Some(vec!["ch1".to_string(), "ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ) + }; + "to heartbeating on joined" + )] + #[test_case( + PresenceState::Stopped { + input: PresenceInput::new( + &Some(vec!["ch1".to_string(), "ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ) + }, + PresenceEvent::Left { + channels: Some(vec!["ch1".to_string()]), + channel_groups: None, + }, + PresenceState::Stopped { + input: PresenceInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ) + }; + "to heartbeating on left" + )] + #[test_case( + PresenceState::Stopped { + input: PresenceInput::new( + &Some(vec!["ch1".to_string(), "ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ) + }, + PresenceEvent::Left { + channels: Some(vec!["ch1".to_string(), "ch2".to_string()]), + channel_groups: Some(vec!["gr1".to_string(), "gr2".to_string()]), + }, + PresenceState::Inactive; + "to inactive on left for all channels and groups" + )] + #[test_case( + PresenceState::Stopped { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }, + PresenceEvent::Reconnect, + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }; + "to heartbeating on reconnect" + )] + #[test_case( + PresenceState::Stopped { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }, + PresenceEvent::LeftAll, + PresenceState::Inactive; + "to inactive on left all" + )] + #[test_case( + PresenceState::Stopped { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }, + PresenceEvent::Joined { + channels: Some(vec!["ch1".to_string()]), + channel_groups: Some(vec!["gr1".to_string()]), + }, + PresenceState::Stopped { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }; + "to not change on joined with same channels and groups" + )] + #[test_case( + PresenceState::Stopped { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }, + PresenceEvent::Left { + channels: None, + channel_groups: Some(vec!["gr3".to_string()]), + }, + PresenceState::Stopped { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }; + "to not change on left with unknown channels and groups" + )] + #[test_case( + PresenceState::Stopped { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }, + PresenceEvent::Disconnect, + PresenceState::Stopped { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }; + "to not change on unexpected event" + )] + #[tokio::test] + async fn transition_for_stopped_state( + init_state: PresenceState, + event: PresenceEvent, + target_state: PresenceState, + ) { + let engine = event_engine(init_state.clone()); + assert!(matches!(init_state, PresenceState::Stopped { .. })); + assert_eq!(engine.current_state(), init_state); + + // Process event. + engine.process(&event); + + assert_eq!(engine.current_state(), target_state); + } + + #[test_case( + PresenceState::Failed { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + PresenceEvent::Joined { + channels: Some(vec!["ch2".to_string()]), + channel_groups: Some(vec!["gr2".to_string()]), + }, + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string(), "ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ) + }; + "to heartbeating on joined" + )] + #[test_case( + PresenceState::Failed { + input: PresenceInput::new( + &Some(vec!["ch1".to_string(), "ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ), + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + PresenceEvent::Left { + channels: Some(vec!["ch1".to_string()]), + channel_groups: None, + }, + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ) + }; + "to heartbeating on left" + )] + #[test_case( + PresenceState::Failed { + input: PresenceInput::new( + &Some(vec!["ch1".to_string(), "ch2".to_string()]), + &Some(vec!["gr1".to_string(), "gr2".to_string()]) + ), + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + PresenceEvent::Left { + channels: Some(vec!["ch1".to_string(), "ch2".to_string()]), + channel_groups: Some(vec!["gr1".to_string(), "gr2".to_string()]), + }, + PresenceState::Inactive; + "to inactive on left for all channels and groups" + )] + #[test_case( + PresenceState::Failed { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + PresenceEvent::Reconnect, + PresenceState::Heartbeating { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }; + "to heartbeating on reconnect" + )] + #[test_case( + PresenceState::Failed { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + PresenceEvent::Disconnect, + PresenceState::Stopped { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ) + }; + "to stopped on disconnect" + )] + #[test_case( + PresenceState::Failed { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + PresenceEvent::LeftAll, + PresenceState::Inactive; + "to inactive on left all" + )] + #[test_case( + PresenceState::Failed { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + PresenceEvent::Joined { + channels: Some(vec!["ch1".to_string()]), + channel_groups: Some(vec!["gr1".to_string()]), + }, + PresenceState::Failed { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }; + "to not change on joined with same channels and groups" + )] + #[test_case( + PresenceState::Failed { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + PresenceEvent::Left { + channels: None, + channel_groups: Some(vec!["gr3".to_string()]), + }, + PresenceState::Failed { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }; + "to not change on left with unknown channels and groups" + )] + #[test_case( + PresenceState::Failed { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }, + PresenceEvent::HeartbeatSuccess, + PresenceState::Failed { + input: PresenceInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), + reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, + }; + "to not change on unexpected event" + )] + #[tokio::test] + async fn transition_for_failed_state( + init_state: PresenceState, + event: PresenceEvent, + target_state: PresenceState, + ) { + let engine = event_engine(init_state.clone()); + assert!(matches!(init_state, PresenceState::Failed { .. })); + assert_eq!(engine.current_state(), init_state); + + // Process event. + engine.process(&event); + + assert_eq!(engine.current_state(), target_state); + } +} diff --git a/src/dx/presence/event_engine/types.rs b/src/dx/presence/event_engine/types.rs new file mode 100644 index 00000000..299a1a38 --- /dev/null +++ b/src/dx/presence/event_engine/types.rs @@ -0,0 +1,554 @@ +//! Presence event engine module types. +//! +//! This module contains the [`PresenceInput`] type, which represents +//! user-provided channels and groups for which `user_id` presence should be +//! managed. + +use crate::{ + core::PubNubError, + lib::{ + alloc::{collections::HashSet, string::String, vec::Vec}, + core::ops::{Add, Sub}, + }, +}; + +/// User-provided channels and groups for presence. +/// +/// Object contains information about channels and groups which should be used +/// with presence event engine states. +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct PresenceInput { + /// Optional list of channels. + /// + /// List of channels for which `user_id` presence should be managed. + pub channels: Option>, + + /// Optional list of channel groups. + /// + /// List of channel groups for which `user_id` presence should be managed. + pub channel_groups: Option>, + + /// Whether user input is empty or not. + pub is_empty: bool, +} + +impl PresenceInput { + pub fn new(channels: &Option>, channel_groups: &Option>) -> Self { + let channels = channels.as_ref().map(|channels| { + channels.iter().fold(HashSet::new(), |mut acc, channel| { + acc.insert(channel.clone()); + acc + }) + }); + let channel_groups = channel_groups.as_ref().map(|groups| { + groups.iter().fold(HashSet::new(), |mut acc, group| { + acc.insert(group.clone()); + acc + }) + }); + + let channel_groups_is_empty = channel_groups.as_ref().map_or(true, |set| set.is_empty()); + let channels_is_empty = channels.as_ref().map_or(true, |set| set.is_empty()); + + Self { + channels, + channel_groups, + is_empty: channel_groups_is_empty && channels_is_empty, + } + } + + pub fn channels(&self) -> Option> { + self.channels.clone().map(|ch| ch.into_iter().collect()) + } + + pub fn channel_groups(&self) -> Option> { + self.channel_groups + .clone() + .map(|ch| ch.into_iter().collect()) + } + + fn join_sets( + &self, + lhs: &Option>, + rhs: &Option>, + ) -> Option> { + match (lhs, rhs) { + (Some(lhs), Some(rhs)) => Some(lhs.iter().cloned().chain(rhs.to_owned()).collect()), + (Some(lhs), None) => Some(lhs.to_owned()), + (None, Some(rhs)) => Some(rhs.to_owned()), + _ => None, + } + } + + fn sub_sets( + &self, + lhs: &Option>, + rhs: &Option>, + ) -> Option> { + match (lhs.to_owned(), rhs.to_owned()) { + (Some(lhs), Some(rhs)) => Some(&lhs - &rhs).filter(|diff| !diff.is_empty()), + (Some(lhs), None) => Some(lhs), + _ => None, + } + } +} + +impl Add for PresenceInput { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + let channel_groups = self.join_sets(&self.channel_groups, &rhs.channel_groups); + let channels = self.join_sets(&self.channels, &rhs.channels); + let channel_groups_is_empty = channel_groups.as_ref().map_or(true, |set| set.is_empty()); + let channels_is_empty = channels.as_ref().map_or(true, |set| set.is_empty()); + + Self { + channels, + channel_groups, + is_empty: channel_groups_is_empty && channels_is_empty, + } + } +} + +impl Sub for PresenceInput { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + let channel_groups = self.sub_sets(&self.channel_groups, &rhs.channel_groups); + let channels = self.sub_sets(&self.channels, &rhs.channels); + let channel_groups_is_empty = channel_groups.as_ref().map_or(true, |set| set.is_empty()); + let channels_is_empty = channels.as_ref().map_or(true, |set| set.is_empty()); + + Self { + channels, + channel_groups, + is_empty: channel_groups_is_empty && channels_is_empty, + } + } +} + +#[cfg(feature = "std")] +#[derive(Clone)] +/// Presence event engine data. +/// +/// Data objects are used by the presence event engine to communicate between +/// components. +pub(crate) struct PresenceParameters<'execution> { + /// List of channel for which `user_id` presence should be announced. + pub channels: &'execution Option>, + + /// List of channel groups for which `user_id` presence should be announced. + pub channel_groups: &'execution Option>, + + /// How many consequent retry attempts has been made. + pub attempt: u8, + + /// Reason why previous request created by presence event engine failed. + pub reason: Option, + + /// Effect identifier. + /// + /// Identifier of effect which requested to create request. + pub effect_id: &'execution str, +} + +#[cfg(test)] +mod it_should { + use super::*; + + #[test] + fn create_empty_input() { + let input = PresenceInput::new(&None, &None); + assert!(input.is_empty); + } + + #[test] + fn create_input_with_unique_channels() { + let input = PresenceInput::new( + &Some(vec![ + "channel-1".into(), + "channel-2".into(), + "channel-1".into(), + ]), + &None, + ); + + assert!(!input.is_empty); + assert_eq!(input.channels().unwrap().len(), 2); + assert_eq!( + input + .channels() + .map(|mut channels| { + channels.sort(); + channels + }) + .unwrap(), + vec!["channel-1".to_string(), "channel-2".to_string()] + ); + } + + #[test] + fn create_input_with_unique_channel_groups() { + let input = PresenceInput::new( + &None, + &Some(vec![ + "channel-group-1".into(), + "channel-group-2".into(), + "channel-group-2".into(), + ]), + ); + + assert!(!input.is_empty); + assert_eq!(input.channel_groups().unwrap().len(), 2); + assert_eq!( + input + .channel_groups() + .map(|mut groups| { + groups.sort(); + groups + }) + .unwrap(), + vec!["channel-group-1".to_string(), "channel-group-2".to_string()] + ); + } + + #[test] + fn add_unique_channels_to_empty_input() { + let empty_input = PresenceInput::new(&None, &None); + let input = PresenceInput::new( + &Some(vec![ + "channel-1".into(), + "channel-2".into(), + "channel-1".into(), + ]), + &None, + ); + + assert!(!input.is_empty); + + let joint_input = empty_input + input; + + assert!(!joint_input.is_empty); + assert_eq!(joint_input.channels().unwrap().len(), 2); + assert_eq!( + joint_input + .channels() + .map(|mut channels| { + channels.sort(); + channels + }) + .unwrap(), + vec!["channel-1".to_string(), "channel-2".to_string()] + ); + assert!(joint_input.channel_groups().is_none()); + } + + #[test] + fn add_unique_channel_groups_to_empty_input() { + let empty_input = PresenceInput::new(&None, &None); + let input = PresenceInput::new( + &None, + &Some(vec![ + "channel-group-1".into(), + "channel-group-2".into(), + "channel-group-2".into(), + ]), + ); + + assert!(!input.is_empty); + + let joint_input = empty_input + input; + + assert!(!joint_input.is_empty); + assert!(joint_input.channels().is_none()); + assert_eq!(joint_input.channel_groups().unwrap().len(), 2); + assert_eq!( + joint_input + .channel_groups() + .map(|mut groups| { + groups.sort(); + groups + }) + .unwrap(), + vec!["channel-group-1".to_string(), "channel-group-2".to_string()] + ); + } + + #[test] + fn add_unique_channels_and_channel_groups_to_existing_input() { + let existing_input = PresenceInput::new( + &Some(vec![ + "channel-1".into(), + "channel-4".into(), + "channel-2".into(), + ]), + &Some(vec![ + "channel-group-1".into(), + "channel-group-3".into(), + "channel-group-5".into(), + ]), + ); + let input = PresenceInput::new( + &Some(vec![ + "channel-1".into(), + "channel-2".into(), + "channel-1".into(), + ]), + &Some(vec![ + "channel-group-1".into(), + "channel-group-2".into(), + "channel-group-2".into(), + ]), + ); + + assert!(!existing_input.is_empty); + assert!(!input.is_empty); + + let joint_input = existing_input + input; + + assert!(!joint_input.is_empty); + assert_eq!(joint_input.channels().unwrap().len(), 3); + assert_eq!(joint_input.channel_groups().unwrap().len(), 4); + assert_eq!( + joint_input + .channels() + .map(|mut channels| { + channels.sort(); + channels + }) + .unwrap(), + vec![ + "channel-1".to_string(), + "channel-2".to_string(), + "channel-4".to_string() + ] + ); + assert_eq!( + joint_input + .channel_groups() + .map(|mut groups| { + groups.sort(); + groups + }) + .unwrap(), + vec![ + "channel-group-1".to_string(), + "channel-group-2".to_string(), + "channel-group-3".to_string(), + "channel-group-5".to_string() + ] + ); + } + + #[test] + fn remove_channels_from_empty_input() { + let empty_input = PresenceInput::new(&None, &None); + let input = PresenceInput::new( + &Some(vec![ + "channel-1".into(), + "channel-2".into(), + "channel-1".into(), + ]), + &None, + ); + + assert!(!input.is_empty); + + let diff_input = empty_input - input; + + assert!(diff_input.is_empty); + assert!(diff_input.channels().is_none()); + assert!(diff_input.channel_groups().is_none()); + } + + #[test] + fn remove_channel_groups_from_empty_input() { + let empty_input = PresenceInput::new(&None, &None); + let input = PresenceInput::new( + &None, + &Some(vec![ + "channel-group-1".into(), + "channel-group-2".into(), + "channel-group-1".into(), + ]), + ); + + assert!(!input.is_empty); + + let diff_input = empty_input - input; + + assert!(diff_input.is_empty); + assert!(diff_input.channels().is_none()); + assert!(diff_input.channel_groups().is_none()); + } + + #[test] + fn remove_unique_channels_from_existing_input() { + let existing_input = PresenceInput::new( + &Some(vec![ + "channel-1".into(), + "channel-2".into(), + "channel-3".into(), + ]), + &Some(vec![ + "channel-group-1".into(), + "channel-group-2".into(), + "channel-group-3".into(), + ]), + ); + let input = PresenceInput::new(&Some(vec!["channel-2".into(), "channel-2".into()]), &None); + + assert!(!existing_input.is_empty); + assert!(!input.is_empty); + + let diff_input = existing_input - input; + + assert!(!diff_input.is_empty); + assert_eq!(diff_input.channels().unwrap().len(), 2); + assert_eq!(diff_input.channel_groups().unwrap().len(), 3); + assert_eq!( + diff_input + .channels() + .map(|mut channels| { + channels.sort(); + channels + }) + .unwrap(), + vec!["channel-1".to_string(), "channel-3".to_string()] + ); + assert_eq!( + diff_input + .channel_groups() + .map(|mut groups| { + groups.sort(); + groups + }) + .unwrap(), + vec![ + "channel-group-1".to_string(), + "channel-group-2".to_string(), + "channel-group-3".to_string(), + ] + ); + } + + #[test] + fn remove_unique_channel_groups_from_existing_input() { + let existing_input = PresenceInput::new( + &Some(vec![ + "channel-1".into(), + "channel-2".into(), + "channel-3".into(), + ]), + &Some(vec![ + "channel-group-1".into(), + "channel-group-2".into(), + "channel-group-3".into(), + ]), + ); + let input = PresenceInput::new(&None, &Some(vec!["channel-group-1".into()])); + + assert!(!existing_input.is_empty); + assert!(!input.is_empty); + + let diff_input = existing_input - input; + + assert!(!diff_input.is_empty); + assert_eq!(diff_input.channels().unwrap().len(), 3); + assert_eq!(diff_input.channel_groups().unwrap().len(), 2); + assert_eq!( + diff_input + .channels() + .map(|mut channels| { + channels.sort(); + channels + }) + .unwrap(), + vec![ + "channel-1".to_string(), + "channel-2".to_string(), + "channel-3".to_string() + ] + ); + assert_eq!( + diff_input + .channel_groups() + .map(|mut groups| { + groups.sort(); + groups + }) + .unwrap(), + vec!["channel-group-2".to_string(), "channel-group-3".to_string(),] + ); + } + + #[test] + fn remove_unique_channels_and_channel_groups_from_existing_input() { + let existing_input = PresenceInput::new( + &Some(vec![ + "channel-1".into(), + "channel-2".into(), + "channel-3".into(), + ]), + &Some(vec![ + "channel-group-1".into(), + "channel-group-2".into(), + "channel-group-3".into(), + ]), + ); + let input = PresenceInput::new( + &Some(vec!["channel-3".into()]), + &Some(vec!["channel-group-2".into(), "channel-group-3".into()]), + ); + + assert!(!existing_input.is_empty); + assert!(!input.is_empty); + + let diff_input = existing_input - input; + + assert!(!diff_input.is_empty); + assert_eq!(diff_input.channels().unwrap().len(), 2); + assert_eq!(diff_input.channel_groups().unwrap().len(), 1); + assert_eq!( + diff_input + .channels() + .map(|mut channels| { + channels.sort(); + channels + }) + .unwrap(), + vec!["channel-1".to_string(), "channel-2".to_string(),] + ); + assert_eq!( + diff_input + .channel_groups() + .map(|mut groups| { + groups.sort(); + groups + }) + .unwrap(), + vec!["channel-group-1".to_string(),] + ); + } + + #[test] + fn remove_all_channels_and_channel_groups_from_existing_input() { + let existing_input = PresenceInput::new( + &Some(vec!["channel-1".into(), "channel-2".into()]), + &Some(vec!["channel-group-1".into(), "channel-group-2".into()]), + ); + let input = PresenceInput::new( + &Some(vec!["channel-1".into(), "channel-2".into()]), + &Some(vec!["channel-group-1".into(), "channel-group-2".into()]), + ); + + assert!(!existing_input.is_empty); + assert!(!input.is_empty); + + let diff_input = existing_input - input; + + assert!(diff_input.is_empty); + assert!(diff_input.channels().is_none()); + assert!(diff_input.channel_groups().is_none()); + } +} diff --git a/src/dx/presence/mod.rs b/src/dx/presence/mod.rs new file mode 100644 index 00000000..91a9c051 --- /dev/null +++ b/src/dx/presence/mod.rs @@ -0,0 +1,546 @@ +//! # Presence module. +//! +//! The presence module allows retrieving presence information and managing the +//! state in specific channels associated with specific `uuid`. +//! The presence module contains [`SetStateRequestBuilder`] type. + +#[cfg(feature = "std")] +use futures::{ + future::{ready, BoxFuture}, + {select_biased, FutureExt}, +}; +use spin::RwLock; + +use crate::{ + core::{Deserializer, PubNubError, Serialize, Transport}, + dx::pubnub_client::PubNubClientInstance, +}; + +#[doc(inline)] +pub use builders::*; +pub mod builders; + +#[doc(inline)] +pub use result::{HeartbeatResponseBody, HeartbeatResult, LeaveResponseBody, LeaveResult}; +pub mod result; + +#[cfg(feature = "std")] +#[doc(inline)] +pub(crate) use presence_manager::PresenceManager; +#[cfg(feature = "std")] +pub(crate) mod presence_manager; +#[cfg(feature = "std")] +#[doc(inline)] +pub(crate) use event_engine::{ + types::PresenceParameters, PresenceEffectHandler, PresenceEventEngine, PresenceState, +}; +#[cfg(feature = "std")] +pub(crate) mod event_engine; +#[cfg(feature = "std")] +use crate::{ + core::{ + event_engine::{cancel::CancellationTask, EventEngine}, + Runtime, + }, + lib::alloc::sync::Arc, +}; + +impl PubNubClientInstance { + /// Create a heartbeat request builder. + /// + /// This method is used to announce the presence of `user_id` on the + /// provided list of channels and/or groups. + /// + /// Instance of [`HeartbeatRequestsBuilder`] returned. + /// + /// # Example + /// ```rust + /// use pubnub::presence::*; + /// # use pubnub::{Keyset, PubNubClientBuilder}; + /// # use std::collections::HashMap; + /// + /// #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let mut pubnub = // PubNubClient + /// # PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: None, + /// # secret_key: None + /// # }) + /// # .with_user_id("uuid") + /// # .build()?; + /// pubnub + /// .heartbeat() + /// .channels(["lobby".into(), "announce".into()]) + /// .channel_groups(["area-51".into()]) + /// .state(HashMap::>::from( + /// [( + /// "lobby".into(), + /// HashMap::from([("is_admin".into(), false)]) + /// )] + /// )) + /// .execute() + /// .await?; + /// # Ok(()) + /// # } + /// ``` + pub fn heartbeat(&self) -> HeartbeatRequestBuilder { + HeartbeatRequestBuilder { + pubnub_client: Some(self.clone()), + heartbeat: Some(self.config.heartbeat_value), + user_id: Some(self.config.user_id.clone().to_string()), + ..Default::default() + } + } + + /// Create a leave request builder. + /// + /// This method is used to announce `leave` of `user_id` on the provided + /// list of channels and/or groups and update state associated with + /// `user_id` on channels. + /// + /// Instance of [`LeaveRequestBuilder`] returned. + pub(in crate::dx::presence) fn leave(&self) -> LeaveRequestBuilder { + LeaveRequestBuilder { + pubnub_client: Some(self.clone()), + user_id: Some(self.config.user_id.clone().to_string()), + ..Default::default() + } + } + + /// Create a set state request builder. + /// + /// This method is used to update state associated with `user_id` on + /// channels and and channels registered with channel groups. + /// + /// Instance of [`SetStateRequestBuilder`] returned. + /// + /// # Example + /// ```rust + /// use pubnub::presence::*; + /// # use pubnub::{Keyset, PubNubClientBuilder}; + /// # use std::collections::HashMap; + /// + /// #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// # use std::sync::Arc; + /// let mut pubnub = // PubNubClient + /// # PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: None, + /// # secret_key: None + /// # }) + /// # .with_user_id("uuid") + /// # .build()?; + /// pubnub + /// .set_state(HashMap::::from( + /// [("is_admin".into(), false)] + /// )) + /// .channels(["lobby".into(), "announce".into()]) + /// .channel_groups(["area-51".into()]) + /// .execute() + /// .await?; + /// # Ok(()) + /// # } + /// ``` + pub fn set_state(&self, state: S) -> SetStateRequestBuilder + where + S: Serialize + Send + Sync + 'static, + { + SetStateRequestBuilder { + pubnub_client: Some(self.clone()), + state: Some(state.serialize().ok()), + user_id: Some(self.config.user_id.clone().to_string()), + ..Default::default() + } + } + + /// Create a heartbeat request builder. + /// + /// This method is used to update state associated with `user_id` on + /// channels using `heartbeat` operation endpoint. + /// + /// Instance of [`HeartbeatRequestsBuilder`] returned. + pub fn set_state_with_heartbeat(&self, state: U) -> HeartbeatRequestBuilder + where + U: Serialize + Send + Sync + 'static, + { + self.heartbeat().state(state) + } +} + +impl PubNubClientInstance +where + T: Transport + Send + 'static, + D: Deserializer + 'static, +{ + /// Announce `join` for `user_id` on provided channels and groups. + #[cfg(feature = "std")] + #[allow(dead_code)] + pub(crate) fn announce_join( + &self, + channels: Option>, + channel_groups: Option>, + ) { + self.configure_presence(); + + { + let slot = self.presence.read(); + if let Some(presence) = slot.as_ref() { + presence.announce_join(channels, channel_groups); + } + }; + } + + /// Announce `leave` for `user_id` on provided channels and groups. + #[cfg(feature = "std")] + #[allow(dead_code)] + pub(crate) fn announce_left( + &self, + channels: Option>, + channel_groups: Option>, + ) { + self.configure_presence(); + + { + let slot = self.presence.read(); + if let Some(presence) = slot.as_ref() { + presence.announce_left(channels, channel_groups); + } + }; + } + + /// Complete presence configuration. + /// + /// Presence configuration used only with presence event engine. + #[cfg(feature = "std")] + pub(crate) fn configure_presence(&self) -> Arc>> { + { + let mut slot = self.presence.write(); + if slot.is_none() { + *slot = Some(PresenceManager::new(self.presence_event_engine(), None)); + } + } + + self.presence.clone() + } + + /// Presence event engine. + /// + /// Prepare presence event engine instance which will be used for `user_id` + /// presence announcement and management. + #[cfg(feature = "std")] + pub(crate) fn presence_event_engine(&self) -> Arc { + let channel_bound = 3; + let (cancel_tx, cancel_rx) = async_channel::bounded::(channel_bound); + let delayed_heartbeat_cancel_rx = cancel_rx.clone(); + let wait_cancel_rx = cancel_rx.clone(); + let runtime = self.runtime.clone(); + let delayed_heartbeat_call_client = self.clone(); + let heartbeat_call_client = self.clone(); + let leave_call_client = self.clone(); + let wait_call_client = self.clone(); + let request_retry_delay_policy = self.config.retry_policy.clone(); + let request_retry_policy = self.config.retry_policy.clone(); + let delayed_heartbeat_runtime_sleep = runtime.clone(); + let wait_runtime_sleep = runtime.clone(); + + EventEngine::new( + PresenceEffectHandler::new( + Arc::new(move |parameters| { + Self::heartbeat_call(heartbeat_call_client.clone(), parameters.clone()) + }), + Arc::new(move |parameters| { + let delay_in_secs = request_retry_delay_policy + .retry_delay(¶meters.attempt, parameters.reason.as_ref()); + let inner_runtime_sleep = delayed_heartbeat_runtime_sleep.clone(); + + Self::delayed_heartbeat_call( + delayed_heartbeat_call_client.clone(), + parameters.clone(), + Arc::new(move || { + if let Some(delay) = delay_in_secs { + inner_runtime_sleep.clone().sleep(delay).boxed() + } else { + ready(()).boxed() + } + }), + delayed_heartbeat_cancel_rx.clone(), + ) + }), + Arc::new(move |parameters| { + Self::leave_call(leave_call_client.clone(), parameters.clone()) + }), + Arc::new(move |effect_id| { + let delay_in_secs = wait_call_client.config.heartbeat_interval; + let inner_runtime_sleep = wait_runtime_sleep.clone(); + + Self::wait_call( + effect_id, + Arc::new(move || { + if let Some(delay) = delay_in_secs { + inner_runtime_sleep.clone().sleep(delay).boxed() + } else { + ready(()).boxed() + } + }), + wait_cancel_rx.clone(), + ) + }), + request_retry_policy, + cancel_tx, + ), + PresenceState::Inactive, + runtime, + ) + } + + /// Call to announce `user_id` presence. + #[cfg(feature = "std")] + pub(crate) fn heartbeat_call( + client: Self, + params: PresenceParameters, + ) -> BoxFuture<'static, Result> { + client.heartbeat_request(params).execute().boxed() + } + + /// Call delayed announce of `user_id` presence. + #[cfg(feature = "std")] + pub(crate) fn delayed_heartbeat_call( + client: Self, + params: PresenceParameters, + delay: Arc, + cancel_rx: async_channel::Receiver, + ) -> BoxFuture<'static, Result> + where + F: Fn() -> BoxFuture<'static, ()> + Send + Sync + 'static, + { + let effect_id = params.effect_id.to_owned(); + let cancel_task = CancellationTask::new(cancel_rx, effect_id); + + client + .heartbeat_request(params) + .execute_with_cancel_and_delay(delay, cancel_task) + .boxed() + } + + /// Call announce `leave` for `user_id`. + #[cfg(feature = "std")] + pub(crate) fn leave_call( + client: Self, + params: PresenceParameters, + ) -> BoxFuture<'static, Result> { + let mut request = client.leave(); + + if let Some(channels) = params.channels.clone() { + request = request.channels(channels); + } + + if let Some(channel_groups) = params.channel_groups.clone() { + request = request.channel_groups(channel_groups); + } + + request.execute().boxed() + } + + /// Heartbeat idle. + #[cfg(feature = "std")] + pub(crate) fn wait_call( + effect_id: &str, + delay: Arc, + cancel_rx: async_channel::Receiver, + ) -> BoxFuture<'static, Result<(), PubNubError>> + where + F: Fn() -> BoxFuture<'static, ()> + Send + Sync + 'static, + { + let cancel_task = CancellationTask::new(cancel_rx, effect_id.to_owned()); + + async move { + select_biased! { + _ = cancel_task.wait_for_cancel().fuse() => { + Err(PubNubError::EffectCanceled) + }, + _ = delay().fuse() => Ok(()) + } + } + .boxed() + } + + /// Call to update `state` associated with `user_id`. + #[allow(dead_code)] + pub(crate) fn set_heartbeat_call(client: Self, _params: PresenceParameters, state: U) + where + U: Serialize + Send + Sync + 'static, + { + // TODO: This is still under development and will be part of EE. + #[cfg(feature = "std")] + { + client.configure_presence(); + + let state = state.serialize().ok(); + if let Some(presence) = client.presence.clone().write().as_mut() { + presence.state = state; + } + } + } + + pub(crate) fn heartbeat_request( + &self, + params: PresenceParameters, + ) -> HeartbeatRequestBuilder { + let mut request = self.heartbeat(); + + if let Some(channels) = params.channels.clone() { + request = request.channels(channels); + } + + if let Some(channel_groups) = params.channel_groups.clone() { + request = request.channel_groups(channel_groups); + } + + if let Some(presence) = self.presence.clone().read().as_ref() { + request = request.state_serialized(presence.state.clone()) + } + + request + } +} + +#[cfg(test)] +mod it_should { + use super::*; + use crate::core::{PubNubError, Transport, TransportRequest, TransportResponse}; + use crate::providers::deserialization_serde::DeserializerSerde; + use crate::transport::middleware::PubNubMiddleware; + use crate::{lib::collections::HashMap, Keyset, PubNubClientBuilder}; + + /// Requests handler function type. + type RequestHandler = Box; + + #[derive(Default)] + struct MockTransport { + /// Response which mocked transport should return. + response: Option, + + /// Request handler function which will be called before returning + /// response. + /// + /// Use function to verify request parameters. + request_handler: Option, + } + + #[async_trait::async_trait] + impl Transport for MockTransport { + async fn send(&self, req: TransportRequest) -> Result { + // Calling request handler (if provided). + if let Some(handler) = &self.request_handler { + handler(&req); + } + + Ok(self.response.clone().unwrap_or(transport_response(200))) + } + } + + /// Service response payload. + fn transport_response(status: u16) -> TransportResponse { + TransportResponse { + status, + body: Some(Vec::from(if status < 400 { + "{\"status\": 200, \"message\": \"OK\", \"service\": \"Presence\"}" + } else { + "\"error\":{{\"message\":\"Overall error\",\"source\":\"test\",\"details\":[{{\"message\":\"Error\",\"location\":\"signature\",\"locationType\":\"query\"}}]}}" + })), + ..Default::default() + } + } + + /// Construct test client with mocked transport. + fn client( + with_subscribe_key: bool, + transport: Option, + ) -> PubNubClientInstance, DeserializerSerde> { + PubNubClientBuilder::with_transport(transport.unwrap_or(MockTransport { + response: None, + request_handler: None, + })) + .with_keyset(Keyset { + subscribe_key: if with_subscribe_key { "demo" } else { "" }, + publish_key: None, + secret_key: None, + }) + .with_user_id("user") + .build() + .unwrap() + } + + #[test] + fn not_heartbeat_when_subscribe_key_missing() { + let client = client(false, None); + let request = client.heartbeat().channels(["test".into()]).build(); + + assert!(&client.config.subscribe_key.is_empty()); + assert!(request.is_err()) + } + + #[tokio::test] + async fn send_heartbeat() { + let client = PubNubClientBuilder::with_reqwest_transport() + .with_keyset(Keyset { + subscribe_key: "demo", + publish_key: Some("demo"), + secret_key: None, + }) + .with_user_id("user_id") + .build() + .unwrap(); + + let result = client + .heartbeat() + .state(HashMap::>::from([( + "hello".into(), + HashMap::from([("is_admin".into(), false)]), + )])) + .channels(["hello".into()]) + .user_id("my_user") + .execute() + .await; + + match result { + Ok(_) => {} + Err(err) => panic!("Request should not fail: {err}"), + } + } + + #[tokio::test] + async fn include_state_in_query() { + let transport = MockTransport { + response: None, + request_handler: Some(Box::new(|req| { + assert!(req.query_parameters.contains_key("state")); + assert!(req.query_parameters.get("state").is_some()); + + let state = req.query_parameters.get("state").unwrap(); + assert!(state.contains("channel_a")); + assert!(state.contains("channel_c")); + })), + }; + + let _ = client(true, Some(transport)) + .heartbeat() + .state(HashMap::>::from([ + ( + "channel_a".into(), + HashMap::::from([("value_a".into(), "secret_a".into())]), + ), + ( + "channel_c".into(), + HashMap::::from([("value_c".into(), "secret_c".into())]), + ), + ])) + .channels(["channel_a".into(), "channel_b".into(), "channel_c".into()]) + .execute() + .await; + } +} diff --git a/src/dx/presence/presence_manager.rs b/src/dx/presence/presence_manager.rs new file mode 100644 index 00000000..3cc3a52c --- /dev/null +++ b/src/dx/presence/presence_manager.rs @@ -0,0 +1,125 @@ +//! Presence module configuration. +//! +//! This module contains [`PresenceManager`] which allow user to configure +//! presence / heartbeat module components. + +use crate::{ + dx::presence::event_engine::{PresenceEvent, PresenceEventEngine}, + lib::{ + alloc::sync::Arc, + core::{ + fmt::{Debug, Formatter, Result}, + ops::{Deref, DerefMut}, + }, + }, +}; + +/// Presence manager. +/// +/// [`PubNubClient`] allows to have state associated with `user_id` on provided +/// list of channels and groups. +#[derive(Debug)] +pub(crate) struct PresenceManager { + pub(crate) inner: Arc, +} + +impl PresenceManager { + pub fn new(event_engine: Arc, state: Option>) -> Self { + Self { + inner: Arc::new(PresenceManagerRef { + event_engine, + state, + }), + } + } +} + +impl Deref for PresenceManager { + type Target = PresenceManagerRef; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for PresenceManager { + fn deref_mut(&mut self) -> &mut Self::Target { + Arc::get_mut(&mut self.inner).expect("Presence configuration is not unique.") + } +} + +impl Clone for PresenceManager { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +/// Presence manager. +/// +/// [`PubNubClient`] allows to have state associated with `user_id` on provided +/// list of channels and groups. +pub(crate) struct PresenceManagerRef { + /// Presence event engine. + pub event_engine: Arc, + + /// A state that should be associated with the `user_id`. + /// + /// `state` object should be a `HashMap` with channel names as keys and + /// nested `HashMap` with values. State with heartbeat can be set **only** + /// for channels. + /// + /// # Example: + /// ```rust,no_run + /// # use std::collections::HashMap; + /// # fn main() { + /// let state = HashMap::>::from([( + /// "announce".into(), + /// HashMap::from([ + /// ("is_owner".into(), false), + /// ("is_admin".into(), true) + /// ]) + /// )]); + /// # } + /// ``` + pub state: Option>, +} + +impl PresenceManagerRef { + /// Announce `join` for `user_id` on provided channels and groups. + #[allow(dead_code)] + pub(crate) fn announce_join( + &self, + channels: Option>, + channel_groups: Option>, + ) { + self.event_engine.process(&PresenceEvent::Joined { + channels, + channel_groups, + }) + } + + /// Announce `leave` for `user_id` on provided channels and groups. + #[allow(dead_code)] + pub(crate) fn announce_left( + &self, + channels: Option>, + channel_groups: Option>, + ) { + self.event_engine.process(&PresenceEvent::Left { + channels, + channel_groups, + }) + } +} + +impl Debug for PresenceManagerRef { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + write!( + f, + "PresenceConfiguration {{\n\tevent_engine: {:?}\n}}", + self.event_engine + ) + } +} diff --git a/src/dx/presence/result.rs b/src/dx/presence/result.rs new file mode 100644 index 00000000..a9621b51 --- /dev/null +++ b/src/dx/presence/result.rs @@ -0,0 +1,329 @@ +//! Presence result module. +//! +//! This module contains the [`HeartbeatResult`] type. + +use crate::core::{ + service_response::{APIErrorBody, APISuccessBodyWithMessage, APISuccessBodyWithPayload}, + PubNubError, +}; + +/// The result of a heartbeat announcement operation. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct HeartbeatResult; + +/// Presence service response body for heartbeat. +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(untagged))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum HeartbeatResponseBody { + /// This is a success response body for a announce heartbeat operation in + /// the Presence service. + /// + /// It contains information about the service that have the response and + /// operation result message. + /// + /// # Example + /// ```json + /// { + /// "status": 200, + /// "message": "OK", + /// "service": "Presence" + /// } + /// ``` + SuccessResponse(APISuccessBodyWithMessage), + + /// This is an error response body for a announce heartbeat operation in the + /// Presence service. + /// It contains information about the service that provided the response and + /// details of what exactly was wrong. + /// + /// # Example + /// ```json + /// { + /// "error": { + /// "message": "Invalid signature", + /// "source": "grant", + /// "details": [ + /// { + /// "message": "Client and server produced different signatures for the same inputs.", + /// "location": "signature", + /// "locationType": "query" + /// } + /// ] + /// }, + /// "service": "Access Manager", + /// "status": 403 + /// } + /// ``` + ErrorResponse(APIErrorBody), +} + +impl TryFrom for HeartbeatResult { + type Error = PubNubError; + + fn try_from(value: HeartbeatResponseBody) -> Result { + match value { + HeartbeatResponseBody::SuccessResponse(_) => Ok(HeartbeatResult), + HeartbeatResponseBody::ErrorResponse(resp) => Err(resp.into()), + } + } +} + +/// The result of a leave announcement operation. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LeaveResult; + +/// Presence service response body for leave. +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(untagged))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LeaveResponseBody { + /// This is a success response body for a announce leave operation in + /// the Presence service. + /// + /// It contains information about the service that have the response and + /// operation result message. + /// + /// # Example + /// ```json + /// { + /// "status": 200, + /// "message": "OK", + /// "service": "Presence" + /// } + /// ``` + SuccessResponse(APISuccessBodyWithMessage), + + /// This is an error response body for a announce leave operation in the + /// Presence service. + /// It contains information about the service that provided the response and + /// details of what exactly was wrong. + /// + /// # Example + /// ```json + /// { + /// "error": { + /// "message": "Invalid signature", + /// "source": "grant", + /// "details": [ + /// { + /// "message": "Client and server produced different signatures for the same inputs.", + /// "location": "signature", + /// "locationType": "query" + /// } + /// ] + /// }, + /// "service": "Access Manager", + /// "status": 403 + /// } + /// ``` + ErrorResponse(APIErrorBody), +} + +impl TryFrom for LeaveResult { + type Error = PubNubError; + + fn try_from(value: LeaveResponseBody) -> Result { + match value { + LeaveResponseBody::SuccessResponse(_) => Ok(LeaveResult), + LeaveResponseBody::ErrorResponse(resp) => Err(resp.into()), + } + } +} + +/// The result of a set state operation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SetStateResult { + /// State which has been associated for `user_id` with channel(s) or channel + /// group(s). + #[cfg(feature = "serde")] + state: serde_json::Value, + + /// State which has been associated for `user_id` with channel(s) or channel + /// group(s). + #[cfg(not(feature = "serde"))] + state: Vec, +} + +/// Set state service response body for heartbeat. +#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(untagged))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SetStateResponseBody { + /// This is a success response body for a set state heartbeat operation in + /// the Presence service. + /// + /// It contains information about the service that have the response, + /// operation result message and state which has been associated for + /// `user_id` with channel(s) or channel group(s). + /// + /// # Example + /// ```json + /// { + /// "status": 200, + /// "message": "OK", + /// "payload": { + /// "key-1": "value-1", + /// "key-2": "value-2" + /// } + /// "service": "Presence" + /// } + /// ``` + #[cfg(feature = "serde")] + SuccessResponse(APISuccessBodyWithPayload), + + /// This is a success response body for a set state heartbeat operation in + /// the Presence service. + /// + /// It contains information about the service that have the response, + /// operation result message and state which has been associated for + /// `user_id` with channel(s) or channel group(s). + /// + /// # Example + /// ```json + /// { + /// "status": 200, + /// "message": "OK", + /// "payload": { + /// "key-1": "value-1", + /// "key-2": "value-2" + /// } + /// "service": "Presence" + /// } + /// ``` + #[cfg(not(feature = "serde"))] + SuccessResponse(APISuccessBodyWithPayload>), + + /// This is an error response body for a set state operation in the Presence + /// service. + /// + /// It contains information about the service that provided the response and + /// details of what exactly was wrong. + /// + /// # Example + /// ```json + /// { + /// "error": { + /// "message": "Invalid signature", + /// "source": "grant", + /// "details": [ + /// { + /// "message": "Client and server produced different signatures for the same inputs.", + /// "location": "signature", + /// "locationType": "query" + /// } + /// ] + /// }, + /// "service": "Access Manager", + /// "status": 403 + /// } + /// ``` + ErrorResponse(APIErrorBody), +} + +impl TryFrom for SetStateResult { + type Error = PubNubError; + + fn try_from(value: SetStateResponseBody) -> Result { + match value { + SetStateResponseBody::SuccessResponse(response) => Ok(SetStateResult { + state: response.payload, + }), + SetStateResponseBody::ErrorResponse(resp) => Err(resp.into()), + } + } +} + +#[cfg(test)] +mod it_should { + use std::collections::HashMap; + + use super::*; + + #[test] + fn parse_heartbeat_response() { + let body = HeartbeatResponseBody::SuccessResponse(APISuccessBodyWithMessage { + status: 200, + message: "OK".into(), + service: "Presence".into(), + }); + let result: Result = body.try_into(); + + assert_eq!(result.unwrap(), HeartbeatResult); + } + + #[test] + fn parse_heartbeat_error_response() { + let body = HeartbeatResponseBody::ErrorResponse(APIErrorBody::AsObjectWithService { + status: 400, + error: true, + service: "service".into(), + message: "error".into(), + }); + let result: Result = body.try_into(); + + assert!(result.is_err()); + } + + #[test] + fn parse_leave_response() { + let body = LeaveResponseBody::SuccessResponse(APISuccessBodyWithMessage { + status: 200, + message: "OK".into(), + service: "Presence".into(), + }); + let result: Result = body.try_into(); + + assert_eq!(result.unwrap(), LeaveResult); + } + + #[test] + fn parse_leave_error_response() { + let body = LeaveResponseBody::ErrorResponse(APIErrorBody::AsObjectWithService { + status: 400, + error: true, + service: "service".into(), + message: "error".into(), + }); + let result: Result = body.try_into(); + + assert!(result.is_err()); + } + + #[test] + fn parse_set_state_response() { + use serde_json::json; + + let payload_value = json!(HashMap::::from([( + "key".into(), + "value".into() + )])); + let body = SetStateResponseBody::SuccessResponse(APISuccessBodyWithPayload { + status: 200, + message: "OK".into(), + payload: payload_value.clone(), + service: "Presence".into(), + }); + let result: Result = body.try_into(); + + assert!(payload_value.is_object()); + assert_eq!( + result.unwrap(), + SetStateResult { + state: payload_value + } + ); + } + + #[test] + fn parse_set_state_error_response() { + let body = SetStateResponseBody::ErrorResponse(APIErrorBody::AsObjectWithService { + status: 400, + error: true, + service: "service".into(), + message: "error".into(), + }); + let result: Result = body.try_into(); + + assert!(result.is_err()); + } +} diff --git a/src/dx/publish/builders.rs b/src/dx/publish/builders.rs index 337a34cc..614fa0dd 100644 --- a/src/dx/publish/builders.rs +++ b/src/dx/publish/builders.rs @@ -1,22 +1,22 @@ -//! Publish builders module. +//! # Publish builders module. //! //! This module contains all builders for the publish operation. -use super::PublishResponseBody; -#[cfg(feature = "serde")] -use crate::providers::deserialization_serde::SerdeDeserializer; +use derive_builder::Builder; + use crate::{ - core::{Deserializer, Serialize}, + core::Serialize, dx::pubnub_client::PubNubClientInstance, lib::{alloc::string::String, collections::HashMap}, }; -use derive_builder::Builder; /// The [`PublishMessageBuilder`] is used to publish a message to a channel. /// -/// This struct is used by the [`publish_message`] method of the [`PubNubClient`]. +/// This struct is used by the [`publish_message`] method of the +/// [`PubNubClient`]. /// The [`publish_message`] method is used to publish a message to a channel. -/// The [`PublishMessageBuilder`] is used to build the request that is sent to the [`PubNub`] network. +/// The [`PublishMessageBuilder`] is used to build the request that is sent to +/// the [`PubNub`] network. /// /// # Examples /// ```rust @@ -46,149 +46,50 @@ use derive_builder::Builder; /// [`publish_message`]: crate::dx::PubNubClient::publish_message` /// [`PubNubClient`]: crate::dx::PubNubClient /// [`PubNub`]:https://www.pubnub.com/ -pub struct PublishMessageBuilder +pub struct PublishMessageBuilder where M: Serialize, { - pub(super) pub_nub_client: PubNubClientInstance, + /// Current client which can provide transportation to perform the request. + /// + /// This field is used to get [`Transport`] to perform the request. + pub(super) pub_nub_client: PubNubClientInstance, pub(super) message: M, pub(super) seqn: u16, } -impl PublishMessageBuilder +impl PublishMessageBuilder where M: Serialize, { - /// The [`channel`] method is used to set the channel to publish the message to. + /// The [`channel`] method is used to set the channel to publish the message + /// to. /// /// [`channel`]: crate::dx::publish::PublishMessageBuilder::channel - #[cfg(feature = "serde")] - pub fn channel(self, channel: S) -> PublishMessageViaChannelBuilder + pub fn channel(self, channel: S) -> PublishMessageViaChannelBuilder where S: Into, { - PublishMessageViaChannelBuilder:: { + PublishMessageViaChannelBuilder:: { pub_nub_client: Some(self.pub_nub_client), seqn: Some(self.seqn), ..Default::default() } .message(self.message) .channel(channel.into()) - .deserialize_with(SerdeDeserializer) - } - - /// The [`channel`] method is used to set the channel to publish the message to. - /// - /// [`channel`]: crate::dx::publish::PublishMessageBuilder::channel - - #[cfg(not(feature = "serde"))] - pub fn channel(self, channel: S) -> PublishMessageDeserializerBuilder - where - S: Into, - { - PublishMessageDeserializerBuilder { - pub_nub_client: self.pub_nub_client, - message: self.message, - seqn: self.seqn, - channel: channel.into(), - } } } -/// The [`PublishMessageDeserializer`] adds the deserializer to the [`PublishMessageBuilder`]. -/// -/// This struct is used to publish a message to a channel. It is used by the [`publish_message`] method of the [`PubNubClient`]. -/// -/// The [`publish_message`] method is used to publish a message to a channel. -/// -/// See more information in the [`PublishMessageBuilder`] struct and the [`Deserializer`] trait. +/// The [`PublishMessageViaChannelBuilder`] is is next step in the publish +/// process. +/// The [`PublishMessageViaChannelBuilder`] is used to build the request to be +/// sent to the [`PubNub`] network. +/// This struct is used to publish a message to a channel. It is used by the +/// [`publish_message`] method of the [`PubNubClient`]. /// /// # Examples /// ```rust -/// # use pubnub::{PubNubClientBuilder, Keyset}; -/// use pubnub::{ -/// dx::publish::{PublishResponse, PublishResponseBody}, -/// core::{Deserializer, PubNubError} -/// }; -/// # #[tokio::main] -/// # async fn main() -> Result<(), Box> { -/// -/// struct MyDeserializer; -/// -/// impl Deserializer for MyDeserializer { -/// fn deserialize(&self, response: &Vec) -> Result { -/// // ... -/// # Ok(PublishResponse) -/// } -/// -/// let mut pubnub = // PubNubClient -/// # PubNubClientBuilder::with_reqwest_transport() -/// # .with_keyset(Keyset{ -/// # subscribe_key: "demo", -/// # publish_key: Some("demo"), -/// # secret_key: None, -/// # }) -/// # .with_user_id("user_id") -/// # .build()?; -/// -/// pubnub.publish_message("hello world!") -/// .channel("my_channel") -/// .deserialize_with(MyDeserializer) -/// .execute() -/// .await?; -/// # Ok(()) -/// # } -/// ``` -/// -/// [`PublishMessageDeserializer`]: crate::dx::publish::PublishMessageDeserializer -/// [`publish_message`]: crate::dx::PubNubClient::publish_message` -/// [`PubNubClient`]: crate::dx::PubNubClient -/// [`Deserializer`]: crate::core::Deserializer -#[cfg(not(feature = "serde"))] -pub struct PublishMessageDeserializerBuilder -where - M: Serialize, -{ - pub_nub_client: PubNubClientInstance, - message: M, - seqn: u16, - channel: String, -} - -#[cfg(not(feature = "serde"))] -impl PublishMessageDeserializerBuilder -where - M: Serialize, -{ - /// The [`deserialize_with`] method is used to set the deserializer to deserialize the response with. - /// It's important to note that the deserializer must implement the [`Deserializer`] trait for - /// the [`PublishResponse`] type. - /// - /// [`deserialize_with`]: crate::dx::publish::PublishMessageDeserializerBuilder::deserialize_with - /// [`Deserializer`]: crate::core::Deserializer - /// [`PublishResponseBody`]: crate::core::publish::PublishResponseBody - pub fn deserialize_with(self, deserializer: D) -> PublishMessageViaChannelBuilder - where - D: Deserializer, - { - PublishMessageViaChannelBuilder { - pub_nub_client: Some(self.pub_nub_client), - seqn: Some(self.seqn), - deserializer: Some(deserializer), - ..Default::default() - } - .message(self.message) - .channel(self.channel) - } -} - -/// The [`PublishMessageViaChannelBuilder`] is is next step in the publish process. -/// The [`PublishMessageViaChannelBuilder`] is used to build the request to be sent to the [`PubNub`] network. -/// This struct is used to publish a message to a channel. It is used by the [`publish_message`] method of the [`PubNubClient`]. -/// -/// # Examples -/// ```rust -/// # use pubnub::{PubNubClientBuilder, Keyset}; +/// # use pubnub::{Keyset, PubNubClientBuilder}; /// /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { @@ -212,30 +113,24 @@ where /// ``` /// /// [`publish_message`]: crate::dx::PubNubClient::publish_message -/// [`PubNub`]:https://www.pubnub.com/ /// [`PubNubClient`]: crate::dx::PubNubClient +/// [`PubNub`]:https://www.pubnub.com/ #[derive(Builder)] #[builder(pattern = "owned", build_fn(vis = "pub(super)"))] #[cfg_attr(not(feature = "std"), builder(no_std))] pub struct PublishMessageViaChannel where M: Serialize, - D: Deserializer, { + /// Current client which can provide transportation to perform the request. + /// + /// This field is used to get [`Transport`] to perform the request. #[builder(setter(custom))] - pub(super) pub_nub_client: PubNubClientInstance, + pub(super) pub_nub_client: PubNubClientInstance, #[builder(setter(custom))] pub(super) seqn: u16, - /// Deserializer to deserialize the response with. - /// Note that the deserializer must implement the [`Deserializer`] trait for - /// the [`PublishResponseBody`] type. - /// [`Deserializer`]: crate::core::Deserializer - /// [`PublishResponseBody`]: crate::core::publish::PublishResponseBody - #[builder(setter(name = "deserialize_with"))] - pub(super) deserializer: D, - /// Message to publish pub(super) message: M, diff --git a/src/dx/publish/mod.rs b/src/dx/publish/mod.rs index d6323ea1..13ce2141 100644 --- a/src/dx/publish/mod.rs +++ b/src/dx/publish/mod.rs @@ -1,10 +1,12 @@ //! Publish module. //! //! Publish message to a channel. -//! The publish module contains the [`PublishMessageBuilder`] and [`PublishMessageViaChannelBuilder`]. -//! The [`PublishMessageBuilder`] is used to publish a message to a channel. +//! The publish module contains the [`PublishMessageBuilder`] and +//! [`PublishMessageViaChannelBuilder`]. The [`PublishMessageBuilder`] is used +//! to publish a message to a channel. //! -//! This module is accountable for publishing a message to a channel of the [`PubNub`] network. +//! This module is accountable for publishing a message to a channel of the +//! [`PubNub`] network. //! //! [`PublishMessageBuilder`]: crate::dx::publish::PublishMessageBuilder] //! [`PublishMessageViaChannelBuilder`]: crate::dx::publish::PublishMessageViaChannelBuilder] @@ -20,8 +22,6 @@ pub use builders::{ }; pub mod builders; -use self::result::body_to_result; - use crate::{ core::{ utils::{ @@ -29,12 +29,11 @@ use crate::{ headers::{APPLICATION_JSON, CONTENT_TYPE}, }, Cryptor, Deserializer, PubNubError, Serialize, Transport, TransportMethod, - TransportRequest, TransportResponse, + TransportRequest, }, dx::pubnub_client::{PubNubClientInstance, PubNubConfig}, lib::{ alloc::{ - boxed::Box, format, string::{String, ToString}, sync::Arc, @@ -43,16 +42,20 @@ use crate::{ core::ops::Not, }, }; + use base64::{engine::general_purpose, Engine as _}; -impl PubNubClientInstance { +impl PubNubClientInstance +where + D: Deserializer, +{ /// Create a new publish message builder. /// This method is used to publish a message to a channel. /// /// Instance of [`PublishMessageBuilder`] is returned. /// /// # Example - /// ```no_run + /// ``` /// # use pubnub::{PubNubClientBuilder, Keyset}; /// /// # #[tokio::main] @@ -77,7 +80,7 @@ impl PubNubClientInstance { /// ``` /// /// [`PublishMessageBuilder`]: crate::dx::publish::PublishMessageBuilder - pub fn publish_message(&self, message: M) -> PublishMessageBuilder + pub fn publish_message(&self, message: M) -> PublishMessageBuilder where M: Serialize, { @@ -105,7 +108,7 @@ impl PubNubClientInstance { impl PublishMessageViaChannelBuilder where M: Serialize, - D: Deserializer, + D: Deserializer, { fn prepare_context_with_request( self, @@ -115,13 +118,12 @@ where .map_err(|err| PubNubError::general_api_error(err.to_string(), None, None))?; PublishMessageContext::from(instance) - .map_data(|client, _, params| { + .map_data(|client, params| { params.create_transport_request(&client.config, &client.cryptor.clone()) }) .map(|ctx| { Ok(PublishMessageContext { client: ctx.client, - deserializer: ctx.deserializer, data: ctx.data?, }) }) @@ -132,11 +134,11 @@ impl PublishMessageViaChannelBuilder where T: Transport, M: Serialize, - D: Deserializer, + D: Deserializer + 'static, { /// Execute the request and return the result. /// This method is asynchronous and will return a future. - /// The future will resolve to a [`PublishResponse`] or [`PubNubError`]. + /// The future will resolve to a [`PublishResult`] or [`PubNubError`]. /// /// # Example /// ```no_run @@ -163,28 +165,17 @@ where /// # } /// ``` /// - /// [`PublishResponse`]: struct.PublishResponse.html + /// [`PublishResult`]: struct.PublishResult.html /// [`PubNubError`]: enum.PubNubError.html pub async fn execute(self) -> Result { self.prepare_context_with_request()? - .map_data(|client, _, request| Self::send_request(client.clone(), request)) - .map(|async_message| async move { - PublishMessageContext { - client: async_message.client, - deserializer: async_message.deserializer, - data: async_message.data.await, - } + .map(|some| async move { + let deserializer = some.client.deserializer.clone(); + some.data + .send::(&some.client.transport, deserializer) + .await }) .await - .map_data(|_, deserializer, response| response_to_result(deserializer, response?)) - .data - } - - async fn send_request( - client: PubNubClientInstance, - request: TransportRequest, - ) -> Result { - client.transport.send(request).await } } @@ -193,11 +184,11 @@ impl PublishMessageViaChannelBuilder where T: crate::core::blocking::Transport, M: Serialize, - D: Deserializer, + D: Deserializer + 'static, { /// Execute the request and return the result. /// This method is asynchronous and will return a future. - /// The future will resolve to a [`PublishResponse`] or [`PubNubError`]. + /// The future will resolve to a [`PublishResult`] or [`PubNubError`]. /// /// # Example /// ```no_run @@ -223,21 +214,18 @@ where /// # } /// ``` /// - /// [`PublishResponse`]: struct.PublishResponse.html + /// [`PublishResult`]: struct.PublishResult.html /// [`PubNubError`]: enum.PubNubError.html pub fn execute_blocking(self) -> Result { self.prepare_context_with_request()? - .map_data(|client, _, request| Self::send_blocking_request(&client.transport, request)) - .map_data(|_, deserializer, response| response_to_result(deserializer, response?)) + .map_data(|client, request| { + let client = client.clone(); + let deserializer = client.deserializer.clone(); + request + .send_blocking::(&client.transport, deserializer) + }) .data } - - fn send_blocking_request( - transport: &T, - request: TransportRequest, - ) -> Result { - transport.send(request) - } } impl PublishMessageParams @@ -328,8 +316,7 @@ where } struct PublishMessageContext { - client: PubNubClientInstance, - deserializer: D, + client: PubNubClientInstance, data: X, } @@ -337,12 +324,11 @@ impl From> for PublishMessageContext> where M: Serialize, - D: Deserializer, + D: Deserializer, { fn from(value: PublishMessageViaChannel) -> Self { Self { client: value.pub_nub_client, - deserializer: value.deserializer, data: PublishMessageParams { channel: value.channel, message: value.message, @@ -359,23 +345,15 @@ where } } -impl PublishMessageContext -where - D: Deserializer, -{ +impl PublishMessageContext { fn map_data(self, f: F) -> PublishMessageContext where - F: FnOnce(&PubNubClientInstance, &D, X) -> Y, + F: FnOnce(PubNubClientInstance, X) -> Y, { let client = self.client; - let deserializer = self.deserializer; - let data = f(&client, &deserializer, self.data); + let data = f(client.clone(), self.data); - PublishMessageContext { - client, - deserializer, - data, - } + PublishMessageContext { client, data } } fn map(self, f: F) -> Y @@ -416,47 +394,10 @@ fn serialize_meta(meta: &HashMap) -> String { result } -// TODO: Maybe it will be possible to extract this into a middleware. -// Currently, it's not necessary, but it might be very useful -// to not have to do it manually in each dx module. -fn response_to_result( - deserializer: &D, - response: TransportResponse, -) -> Result -where - D: Deserializer, -{ - response - .body - .as_ref() - .map(|body| { - let deserialize_result = deserializer.deserialize(body); - if deserialize_result.is_err() && response.status >= 500 { - Err(PubNubError::general_api_error( - "Unexpected service response", - None, - Some(Box::new(response.clone())), - )) - } else { - deserialize_result - } - }) - .transpose() - .and_then(|body| { - body.ok_or_else(|| { - PubNubError::general_api_error( - format!("No body in the response! Status code: {}", response.status), - None, - Some(Box::new(response.clone())), - ) - }) - .map(|body| body_to_result(body, response)) - })? -} - #[cfg(test)] mod should { use super::*; + use crate::providers::deserialization_serde::DeserializerSerde; use crate::{ core::TransportResponse, dx::pubnub_client::{PubNubClientInstance, PubNubClientRef, PubNubConfig}, @@ -469,7 +410,7 @@ mod should { #[derive(Default, Debug)] struct MockTransport; - fn client() -> PubNubClientInstance> { + fn client() -> PubNubClientInstance, DeserializerSerde> { #[async_trait::async_trait] impl Transport for MockTransport { async fn send( diff --git a/src/dx/publish/result.rs b/src/dx/publish/result.rs index 8475fc83..c9784b21 100644 --- a/src/dx/publish/result.rs +++ b/src/dx/publish/result.rs @@ -4,8 +4,8 @@ //! The `PublishResult` type is used to represent the result of a publish operation. use crate::{ - core::{APIErrorBody, PubNubError, TransportResponse}, - lib::alloc::{boxed::Box, string::String}, + core::{service_response::APIErrorBody, PubNubError}, + lib::alloc::string::String, }; /// The result of a publish operation. @@ -20,8 +20,9 @@ pub struct PublishResult { /// It can be either a tuple with data from the Publish service /// or an [`OtherResponse`] from other services. /// -/// It's used for deserialization of the publish response. This type is an intermediate -/// type between the raw response body and the [`PublishResult`] type. +/// It's used for deserialization of the publish response. This type is an +/// intermediate type between the raw response body and the [`PublishResult`] +/// type. /// /// [`OtherResponse`]: struct.OtherResponse.html /// [`PublishResult`]: struct.PublishResult.html @@ -30,10 +31,11 @@ pub struct PublishResult { #[derive(Debug, Clone, PartialEq, Eq)] pub enum PublishResponseBody { /// The response body of a publish operation in publish service. - /// It contains the error indicator, the message from service and the timetoken - /// in this order. + /// It contains the error indicator, the message from service and the + /// timetoken in this order. /// - /// The error indicator is `1` if the operation was successful and `0` otherwise. + /// The error indicator is `1` if the operation was successful and `0` + /// otherwise. /// /// # Example /// ```json @@ -44,25 +46,19 @@ pub enum PublishResponseBody { ErrorResponse(APIErrorBody), } -pub(super) fn body_to_result( - body: PublishResponseBody, - response: TransportResponse, -) -> Result { - match body { - PublishResponseBody::SuccessResponse(error_indicator, message, timetoken) => { - if error_indicator == 1 { - Ok(PublishResult { timetoken }) - } else { - Err(PubNubError::general_api_error( - message, - Some(response.status), - Some(Box::new(response)), - )) +impl TryFrom for PublishResult { + type Error = PubNubError; + + fn try_from(value: PublishResponseBody) -> Result { + match value { + PublishResponseBody::SuccessResponse(error_indicator, message, timetoken) => { + if error_indicator == 1 { + Ok(PublishResult { timetoken }) + } else { + Err(PubNubError::general_api_error(message, None, None)) + } } - } - PublishResponseBody::ErrorResponse(resp) => { - let error: PubNubError = resp.into(); - Err(error.attach_response(response)) + PublishResponseBody::ErrorResponse(resp) => Err(resp.into()), } } } @@ -70,6 +66,30 @@ pub(super) fn body_to_result( #[cfg(test)] mod should { use super::*; + use crate::core::TransportResponse; + + fn body_to_result( + body: PublishResponseBody, + response: TransportResponse, + ) -> Result { + match body { + PublishResponseBody::SuccessResponse(error_indicator, message, timetoken) => { + if error_indicator == 1 { + Ok(PublishResult { timetoken }) + } else { + Err(PubNubError::general_api_error( + message, + Some(response.status), + Some(Box::new(response)), + )) + } + } + PublishResponseBody::ErrorResponse(resp) => { + let error: PubNubError = resp.into(); + Err(error.attach_response(response)) + } + } + } #[test] fn parse_publish_response() { diff --git a/src/dx/pubnub_client.rs b/src/dx/pubnub_client.rs index a4027d74..3f054d97 100644 --- a/src/dx/pubnub_client.rs +++ b/src/dx/pubnub_client.rs @@ -7,11 +7,27 @@ //! [`PubNub API`]: https://www.pubnub.com/docs //! [`pubnub`]: ../index.html -use crate::core::Cryptor; -#[cfg(all(feature = "subscribe", feature = "std"))] -use crate::dx::subscribe::SubscriptionConfiguration; +#[cfg(all(feature = "presence", feature = "std"))] +use crate::presence::PresenceManager; +#[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] use crate::{ - core::{PubNubError, Transport}, + core::runtime::RuntimeSupport, providers::futures_tokio::RuntimeTokio, + subscribe::SubscriptionManager, +}; + +#[cfg(not(feature = "serde"))] +use crate::core::Deserializer; +#[cfg(feature = "serde")] +use crate::providers::deserialization_serde::DeserializerSerde; +#[cfg(feature = "reqwest")] +use crate::transport::TransportReqwest; + +// TODO: Retry policy would be implemented for `no_std` event engine +#[cfg(feature = "std")] +use crate::core::RequestRetryPolicy; + +use crate::{ + core::{Cryptor, PubNubError}, lib::{ alloc::{ string::{String, ToString}, @@ -24,10 +40,7 @@ use crate::{ use derive_builder::Builder; use log::info; use spin::{Mutex, RwLock}; - -// TODO: Retry policy would be implemented for `no_std` event engine -#[cfg(feature = "std")] -use crate::core::RequestRetryPolicy; +use uuid::Uuid; /// PubNub client /// @@ -36,9 +49,11 @@ use crate::core::RequestRetryPolicy; /// that implements the [`Transport`] trait. /// /// You can create clients using the [`PubNubClient::with_transport`] -/// You must provide a valid [`Keyset`] with pub/sub keys and a string User ID to identify the client. +/// You must provide a valid [`Keyset`] with pub/sub keys and a string User ID +/// to identify the client. /// -/// To see available methods, please refer to the [`PubNubClientInstance`] documentation. +/// To see available methods, please refer to the [`PubNubClientInstance`] +/// documentation. /// /// # Examples /// ``` @@ -109,21 +124,24 @@ use crate::core::RequestRetryPolicy; /// [`selected`]: ../index.html#features /// [`Keyset`]: ../core/struct.Keyset.html /// [`PubNubClient::with_transport`]: struct.PubNubClientBuilder.html#method.with_transport`] -pub type PubNubGenericClient = PubNubClientInstance>; +pub type PubNubGenericClient = PubNubClientInstance, D>; /// PubNub client /// /// Client for PubNub API with support for all [`selected`] PubNub features. -/// The client uses [`reqwest`] as a transport layer. +/// The client uses [`reqwest`] as a transport layer and [`serde`] for responses +/// deserialization. /// -/// You can create clients using the [`PubNubClient::with_reqwest_transport`] method. -/// You must provide a valid [`Keyset`] with pub/sub keys and a string User ID to identify the client. +/// You can create clients using the [`PubNubClient::with_reqwest_transport`] +/// method. +/// You must provide a valid [`Keyset`] with pub/sub keys and a string User ID +/// to identify the client. /// /// To see available methods, please refer to the [`PubNubClientInstance`] documentation. /// /// # Examples /// ``` -/// use pubnub::{PubNubClientBuilder, Keyset}; +/// use pubnub::{Keyset, PubNubClientBuilder}; /// /// // note that `with_reqwest_transport` requires `reqwest` feature /// // to be enabled (default) @@ -144,9 +162,9 @@ pub type PubNubGenericClient = PubNubClientInstance>; /// Using your own [`Transport`] implementation: /// /// ``` -/// use pubnub::{PubNubClientBuilder, Keyset}; -/// /// # use pubnub::core::{Transport, TransportRequest, TransportResponse, PubNubError}; +/// use pubnub::{Keyset, PubNubClientBuilder}; +/// /// # struct MyTransport; /// # #[async_trait::async_trait] /// # impl Transport for MyTransport { @@ -192,34 +210,34 @@ pub type PubNubGenericClient = PubNubClientInstance>; /// [`Keyset`]: ../core/struct.Keyset.html /// [`reqwest`]: https://crates.io/crates/reqwest /// [`PubNubClient::with_reqwest_transport`]: struct.PubNubClientBuilder.html#method.with_reqwest_transport -#[cfg(feature = "reqwest")] -pub type PubNubClient = PubNubGenericClient; +#[cfg(all(feature = "reqwest", feature = "serde"))] +pub type PubNubClient = PubNubGenericClient; /// PubNub client raw instance. /// /// This struct contains the actual client state. /// It shouldn't be used directly. Use [`PubNubGenericClient`] or [`PubNubClient`] instead. #[derive(Debug)] -pub struct PubNubClientInstance { - pub(crate) inner: Arc>, +pub struct PubNubClientInstance { + pub(crate) inner: Arc>, } -impl Deref for PubNubClientInstance { - type Target = PubNubClientRef; +impl Deref for PubNubClientInstance { + type Target = PubNubClientRef; fn deref(&self) -> &Self::Target { &self.inner } } -impl DerefMut for PubNubClientInstance { +impl DerefMut for PubNubClientInstance { fn deref_mut(&mut self) -> &mut Self::Target { Arc::get_mut(&mut self.inner) .expect("Multiple mutable references to PubNubClientInstance are not allowed") } } -impl Clone for PubNubClientInstance { +impl Clone for PubNubClientInstance { fn clone(&self) -> Self { Self { inner: Arc::clone(&self.inner), @@ -230,7 +248,8 @@ impl Clone for PubNubClientInstance { /// Client reference /// /// This struct contains the actual client state. -/// It's wrapped in `Arc` by [`PubNubClient`] and uses interior mutability for its internal state. +/// It's wrapped in `Arc` by [`PubNubClient`] and uses interior mutability for +/// its internal state. /// /// Not intended to be used directly. Use [`PubNubClient`] instead. #[derive(Builder, Debug)] @@ -241,11 +260,16 @@ impl Clone for PubNubClientInstance { setter(prefix = "with"), no_std )] -pub struct PubNubClientRef { +pub struct PubNubClientRef { /// Transport layer pub(crate) transport: T, - /// Data cryptor / decryptor + /// [`PubNub API`] responses deserializer + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub(crate) deserializer: Arc, + + /// Data cryptor / decryptor provider #[builder( setter(custom, strip_option), field(vis = "pub(crate)"), @@ -256,7 +280,7 @@ pub struct PubNubClientRef { /// Instance ID #[builder( setter(into), - field(type = "String", build = "Arc::new(Some(self.instance_id))") + field(type = "String", build = "Arc::new(Some(Uuid::new_v4().to_string()))") )] pub(crate) instance_id: Arc>, @@ -275,60 +299,25 @@ pub struct PubNubClientRef { )] pub(crate) auth_token: Arc>, + /// Runtime environment + #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] + #[builder(setter(custom), field(vis = "pub(crate)"))] + pub(crate) runtime: RuntimeSupport, + /// Subscription module configuration #[cfg(all(feature = "subscribe", feature = "std"))] #[builder(setter(skip), field(vis = "pub(crate)"))] - pub(crate) subscription: Arc>>, -} + pub(crate) subscription: Arc>>, -impl PubNubClientInstance { - /// Create a new builder for [`PubNubClient`] - /// - /// # Examples - /// ``` - /// use pubnub::{PubNubClientBuilder, Keyset}; - /// - /// # use pubnub::core::{Transport, TransportRequest, TransportResponse, PubNubError}; - /// # struct MyTransport; - /// # #[async_trait::async_trait] - /// # impl pubnub::core::blocking::Transport for MyTransport { - /// # fn send(&self, _request: TransportRequest) -> Result { - /// # unimplemented!() - /// # } - /// # } - /// # - /// # impl MyTransport { - /// # fn new() -> Self { - /// # Self - /// # } - /// # } + /// Presence / heartbeat event engine. /// - /// # fn main() -> Result<(), PubNubError> { - /// // note that MyTransport must implement the `Transport` trait - /// let transport = MyTransport::new(); - /// - /// let builder = PubNubClientBuilder::with_blocking_transport(transport) - /// .with_keyset(Keyset { - /// publish_key: Some("pub-c-abc123"), - /// subscribe_key: "sub-c-abc123", - /// secret_key: None, - /// }) - /// .with_user_id("my-user-id") - /// .build()?; - /// - /// # Ok(()) - /// # } - /// ``` - #[cfg(feature = "blocking")] - pub fn with_blocking_transport(transport: T) -> PubNubClientBuilder - where - T: crate::core::blocking::Transport + Send + Sync, - { - PubNubClientBuilder { - transport: Some(transport), - } - } + /// State machine which is responsible for `user_id` presence maintenance. + #[cfg(all(feature = "presence", feature = "std"))] + #[builder(setter(skip), field(vis = "pub(crate)"))] + pub(crate) presence: Arc>>, +} +impl PubNubClientInstance { /// Update currently used authentication token. /// /// # Examples @@ -386,15 +375,15 @@ impl PubNubClientInstance { /// ``` pub fn get_token(&self) -> Option { let token = self.auth_token.read().deref().clone(); - token.is_empty().then_some(token) + (!token.is_empty()).then_some(token) } } -impl PubNubClientConfigBuilder { +impl PubNubClientConfigBuilder { /// Set client authentication key. /// /// It returns [`PubNubClientConfigBuilder`] that you can use to set the - /// configuration for the client. This is a part the + /// configuration for the client. This is a part of the /// [`PubNubClientConfigBuilder`]. pub fn with_auth_key(mut self, auth_key: S) -> Self where @@ -407,13 +396,46 @@ impl PubNubClientConfigBuilder { self } + /// `user_id` presence heartbeat. + /// + /// Used to set the presence timeout period. It overrides the default value + /// of 300 seconds for Presence timeout. + /// + /// It returns [`PubNubClientConfigBuilder`] that you can use to set the + /// configuration for the client. This is a part of the + /// [`PubNubClientConfigBuilder`]. + #[cfg(any(feature = "subscribe", feature = "presence"))] + pub fn with_heartbeat_value(mut self, value: u64) -> Self { + if let Some(configuration) = self.config.as_mut() { + configuration.heartbeat_value = value; + configuration.heartbeat_interval = Some(value / 2 - 1); + } + self + } + + /// `user_id` presence announcement interval. + /// + /// Intervals at which `user_id` presence should be announced and should + /// follow this optimal formula: `heartbeat_value / 2 - 1`. + /// + /// It returns [`PubNubClientConfigBuilder`] that you can use to set the + /// configuration for the client. This is a part of the + /// [`PubNubClientConfigBuilder`]. + #[cfg(any(feature = "subscribe", feature = "presence"))] + pub fn with_heartbeat_interval(mut self, interval: u64) -> Self { + if let Some(configuration) = self.config.as_mut() { + configuration.heartbeat_interval = Some(interval); + } + self + } + /// Requests retry policy. /// - /// The retry policy regulates the frequency of request retry attempts and the number of failed - /// attempts that should be retried. + /// The retry policy regulates the frequency of request retry attempts and + /// the number of failed attempts that should be retried. /// /// It returns [`PubNubClientConfigBuilder`] that you can use to set the - /// configuration for the client. This is a part the + /// configuration for the client. This is a part of the /// [`PubNubClientConfigBuilder`]. #[cfg(feature = "std")] pub fn with_retry_policy(mut self, policy: RequestRetryPolicy) -> Self { @@ -426,11 +448,11 @@ impl PubNubClientConfigBuilder { /// Data encryption / decryption /// - /// Cryptor used by client when publish messages / signals and receive them as real-time updates - /// from subscription module. + /// Cryptor used by client when publish messages / signals and receive them + /// as real-time updates from subscription module. /// /// It returns [`PubNubClientConfigBuilder`] that you can use to set the - /// configuration for the client. This is a part the + /// configuration for the client. This is a part of the /// [`PubNubClientConfigBuilder`]. pub fn with_cryptor(mut self, cryptor: C) -> Self where @@ -442,14 +464,20 @@ impl PubNubClientConfigBuilder { } /// Build a [`PubNubClient`] from the builder - pub fn build(self) -> Result>, PubNubError> { + pub fn build(self) -> Result, D>, PubNubError> { self.build_internal() .map_err(|err| PubNubError::ClientInitialization { details: err.to_string(), }) .and_then(|pre_build| { let token = Arc::new(RwLock::new(String::new())); - info!("Client Configuration: \n publish_key: {:?}\n subscribe_key: {}\n user_id: {}\n instance_id: {:?}", pre_build.config.publish_key, pre_build.config.subscribe_key, pre_build.config.user_id, pre_build.instance_id); + info!( + "Client Configuration: \n publish_key: {:?}\n subscribe_key: {}\n user_id: {}\n instance_id: {:?}", + pre_build.config.publish_key, + pre_build.config.subscribe_key, + pre_build.config.user_id, + pre_build.instance_id + ); Ok(PubNubClientRef { transport: PubNubMiddleware { signature_keys: pre_build.config.clone().signature_key_set()?, @@ -459,20 +487,25 @@ impl PubNubClientConfigBuilder { transport: pre_build.transport, auth_token: token.clone(), }, + deserializer: pre_build.deserializer, instance_id: pre_build.instance_id, next_seqn: pre_build.next_seqn, auth_token: token, config: pre_build.config, cryptor: pre_build.cryptor.clone(), + #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] + runtime: pre_build.runtime, + #[cfg(all(feature = "subscribe", feature = "std"))] subscription: Arc::new(RwLock::new(None)), + + #[cfg(all(feature = "presence", feature = "std"))] + presence: Arc::new(RwLock::new(None)), }) }) - .map(|client| { - PubNubClientInstance { - inner: Arc::new(client), - } + .map(|client| PubNubClientInstance { + inner: Arc::new(client), }) } } @@ -486,21 +519,35 @@ pub struct PubNubConfig { /// Subscribe key pub(crate) subscribe_key: String, - /// User ID - pub(crate) user_id: Arc, - /// Publish key pub(crate) publish_key: Option, /// Secret key pub(crate) secret_key: Option, + /// User ID + pub(crate) user_id: Arc, + /// Authorization key pub(crate) auth_key: Option>, /// Request retry policy #[cfg(feature = "std")] pub(crate) retry_policy: RequestRetryPolicy, + + /// `user_id` presence heartbeat. + /// + /// Used to set the presence timeout period. It overrides the default value + /// of 300 seconds for Presence timeout. + #[cfg(any(feature = "subscribe", feature = "presence"))] + pub heartbeat_value: u64, + + /// `user_id` presence announcement interval. + /// + /// Intervals at which `user_id` presence should be announced and should + /// follow this optimal formula: `heartbeat_value / 2 - 1`. + #[cfg(any(feature = "subscribe", feature = "presence"))] + pub heartbeat_interval: Option, } impl PubNubConfig { @@ -531,32 +578,78 @@ impl PubNubConfig { /// that implements the [`Transport`] trait. /// /// You can use the [`Default`] implementation to create a builder -/// with the default transport layer. The `default` method is implemented only -/// when the `reqwest` feature is enabled. +/// with the default transport layer, responses deserializer and runtime +/// environment (if `tokio` feature enabled). The `default` method is +/// implemented only when the `reqwest` and `serde` features is enabled. /// -/// The builder provides methods to set the transport layer and returns the next step -/// of the builder with the remaining parameters. +/// The builder provides methods to set the transport layer and returns the next +/// step of the builder with the remaining parameters. /// /// See [`PubNubClient`] for more information. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct PubNubClientBuilder { - pub(crate) transport: Option, -} +#[derive(Debug, Clone)] +pub struct PubNubClientBuilder; -impl Default for PubNubClientBuilder { - fn default() -> Self { - Self { transport: None } +impl PubNubClientBuilder { + /// Set the transport layer for the client. + /// + /// Returns [`PubNubClientRuntimeBuilder`] where depending from enabled `features` following can be set: + /// * runtime environment + /// * API ket set to access [`PubNub API`]. + /// + /// # Examples + /// ``` + /// # use pubnub::core::{Transport, TransportRequest, TransportResponse, PubNubError}; + /// use pubnub::{Keyset, PubNubClientBuilder}; + /// # + /// # struct MyTransport; + /// # #[async_trait::async_trait] + /// # impl Transport for MyTransport { + /// # async fn send(&self, _request: TransportRequest) -> Result { + /// # unimplemented!() + /// # } + /// # } + /// # impl MyTransport { + /// # fn new() -> Self { + /// # Self + /// # } + /// # } + /// + /// # fn main() -> Result<(), Box> { + /// // note that MyTransport must implement the `Transport` trait + /// let transport = MyTransport::new(); + /// + /// let client = PubNubClientBuilder::with_transport(transport) + /// .with_keyset(Keyset { + /// publish_key: Some("pub-c-abc123"), + /// subscribe_key: "sub-c-abc123", + /// secret_key: None, + /// }) + /// .with_user_id("my-user-id") + /// .build()?; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] + pub fn with_transport(transport: T) -> PubNubClientRuntimeBuilder + where + T: crate::core::Transport, + { + PubNubClientRuntimeBuilder { transport } } -} -impl PubNubClientBuilder { - /// Set the transport layer for the client + /// Set the transport layer for the client. + /// + /// Returns [`PubNubClientDeserializerBuilder`] where depending from enabled `features` following can be set: + /// * [`PubNub API`] response deserializer + /// * API ket set to access [`PubNub API`]. /// /// # Examples /// ``` - /// use pubnub::{PubNubClientBuilder, Keyset}; - /// /// # use pubnub::core::{Transport, TransportRequest, TransportResponse, PubNubError}; + /// use pubnub::{Keyset, PubNubClientBuilder}; + /// # /// # struct MyTransport; /// # #[async_trait::async_trait] /// # impl Transport for MyTransport { @@ -575,32 +668,89 @@ impl PubNubClientBuilder { /// let transport = MyTransport::new(); /// /// let client = PubNubClientBuilder::with_transport(transport) - /// .with_keyset(Keyset { - /// publish_key: Some("pub-c-abc123"), - /// subscribe_key: "sub-c-abc123", - /// secret_key: None, - /// }) - /// .with_user_id("my-user-id") - /// .build()?; + /// .with_keyset(Keyset { + /// publish_key: Some("pub-c-abc123"), + /// subscribe_key: "sub-c-abc123", + /// secret_key: None, + /// }) + /// .with_user_id("my-user-id") + /// .build()?; /// # Ok(()) /// # } /// ``` - pub fn with_transport(transport: T) -> PubNubClientBuilder + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + #[cfg(any( + all(not(feature = "subscribe"), not(feature = "presence")), + not(feature = "std") + ))] + pub fn with_transport(transport: T) -> PubNubClientDeserializerBuilder { + PubNubClientDeserializerBuilder { transport } + } + + /// Set the blocking transport layer for the client. + /// + /// Returns [`PubNubClientRuntimeBuilder`] where depending from enabled `features` following can be set: + /// * runtime environment + /// * API ket set to access [`PubNub API`]. + /// + /// # Examples + /// ``` + /// # use pubnub::core::{blocking::Transport, TransportRequest, TransportResponse, PubNubError}; + /// use pubnub::{Keyset, PubNubClientBuilder}; + /// + /// # struct MyTransport; + /// # impl Transport for MyTransport { + /// # fn send(&self, _request: TransportRequest) -> Result { + /// # unimplemented!() + /// # } + /// # } + /// # impl MyTransport { + /// # fn new() -> Self { + /// # Self + /// # } + /// # } + /// + /// # fn main() -> Result<(), Box> { + /// // note that MyTransport must implement the `Transport` trait + /// let transport = MyTransport::new(); + /// + /// let client = PubNubClientBuilder::with_blocking_transport(transport) + /// .with_keyset(Keyset { + /// publish_key: Some("pub-c-abc123"), + /// subscribe_key: "sub-c-abc123", + /// secret_key: None, + /// }) + /// .with_user_id("my-user-id") + /// .build()?; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + #[cfg(all( + any(feature = "subscribe", feature = "presence"), + feature = "std", + feature = "blocking" + ))] + pub fn with_blocking_transport(transport: T) -> PubNubClientRuntimeBuilder where - T: Transport, + T: crate::core::blocking::Transport + Send + Sync, { - PubNubClientBuilder { - transport: Some(transport), - } + PubNubClientRuntimeBuilder { transport } } - /// Set the blocking transport layer for the client + /// Set the blocking transport layer for the client. + /// + /// Returns [`PubNubClientDeserializerBuilder`] where depending from enabled `features` following can be set: + /// * [`PubNub API`] response deserializer + /// * API ket set to access [`PubNub API`]. /// /// # Examples /// ``` - /// use pubnub::{PubNubClientBuilder, Keyset}; - /// /// # use pubnub::core::{blocking::Transport, TransportRequest, TransportResponse, PubNubError}; + /// use pubnub::{Keyset, PubNubClientBuilder}; + /// # /// # struct MyTransport; /// # impl Transport for MyTransport { /// # fn send(&self, _request: TransportRequest) -> Result { @@ -613,31 +763,60 @@ impl PubNubClientBuilder { /// # } /// # } /// - /// # fn main() -> Result<(), PubNubError> { + /// # fn main() -> Result<(), Box> { /// // note that MyTransport must implement the `Transport` trait /// let transport = MyTransport::new(); /// /// let client = PubNubClientBuilder::with_blocking_transport(transport) - /// .with_keyset(Keyset { - /// publish_key: Some("pub-c-abc123"), - /// subscribe_key: "sub-c-abc123", - /// secret_key: None, - /// }) - /// .with_user_id("my-user-id") - /// .build()?; + /// .with_keyset(Keyset { + /// publish_key: Some("pub-c-abc123"), + /// subscribe_key: "sub-c-abc123", + /// secret_key: None, + /// }) + /// .with_user_id("my-user-id") + /// .build()?; /// # Ok(()) /// # } /// ``` + /// + /// [`PubNub API`]: https://www.pubnub.com/docs #[cfg(feature = "blocking")] - pub fn with_blocking_transport(transport: T) -> PubNubClientBuilder + #[cfg(any( + all(not(feature = "subscribe"), not(feature = "presence")), + not(feature = "std") + ))] + pub fn with_blocking_transport(transport: T) -> PubNubClientDeserializerBuilder where T: crate::core::blocking::Transport + Send + Sync, { - PubNubClientBuilder { - transport: Some(transport), - } + PubNubClientDeserializerBuilder { transport } } +} + +/// PubNub builder for [`PubNubClient`] to set API keys. +/// +/// The builder provides methods to set the [`PubNub API`] keys set and returns the next +/// step of the builder with the remaining parameters. +/// +/// See [`PubNubClient`] for more information. +/// +/// [`PubNub API`]: https://www.pubnub.com/docs +#[derive(Debug, Clone)] +pub struct PubNubClientKeySetBuilder { + /// Transport layer. + pub(crate) transport: T, + + /// [`PubNub API`] responses deserializer + /// + /// [`PubNub API`]: https://www.pubnub.com/docs + pub(crate) deserializer: D, + /// Runtime environment + #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] + pub(crate) runtime: RuntimeSupport, +} + +impl PubNubClientKeySetBuilder { /// Set the keyset for the client /// /// It returns [`PubNubClientUserIdBuilder`] builder that you can use @@ -647,27 +826,331 @@ impl PubNubClientBuilder { /// /// # Examples /// ``` - /// use pubnub::{PubNubClientBuilder, Keyset}; + /// use pubnub::{Keyset, PubNubClientBuilder}; /// + /// # fn main() -> Result<(), Box> { /// // note that with_reqwest_transport is only available when /// // the `reqwest` feature is enabled (default) /// let builder = PubNubClientBuilder::with_reqwest_transport() - /// .with_keyset(Keyset { - /// subscribe_key: "sub-c-abc123", - /// publish_key: Some("pub-c-abc123"), - /// secret_key: None, - /// }); + /// .with_keyset(Keyset { + /// subscribe_key: "sub-c-abc123", + /// publish_key: Some("pub-c-abc123"), + /// secret_key: None, + /// }); + /// # Ok(()) + /// # } /// ``` /// - /// [`PubNubClientUserIdBuilder`]: struct.PubNubClientBuilderKeyset.html + /// [`PubNubClientUserIdBuilder`]: struct.PubNubClientUserIdBuilder.html /// [`Keyset`]: struct.Keyset.html - pub fn with_keyset(self, keyset: Keyset) -> PubNubClientUserIdBuilder + pub fn with_keyset(self, keyset: Keyset) -> PubNubClientUserIdBuilder where S: Into, { PubNubClientUserIdBuilder { transport: self.transport, + deserializer: self.deserializer, keyset, + #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] + runtime: self.runtime, + } + } +} + +/// PubNub builder for [`PubNubClient`] used to set runtime environment. +/// +/// Runtime will be used for detached tasks spawning and delayed task execution. +/// +/// Depending from enabled `features` methods may return: +/// * [`PubNubClientDeserializerBuilder`] to set custom [`PubNub API`] deserializer +/// * [`PubNubClientKeySetBuilder`] to set API keys set to access [`PubNub API`] +/// * [`PubNubClientUserIdBuilder`] to set user id for the client. +/// +/// [`PubNub API`]: https://www.pubnub.com/docs +#[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] +pub struct PubNubClientRuntimeBuilder { + /// Transport layer. + pub(crate) transport: T, +} + +#[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] +impl PubNubClientRuntimeBuilder { + /// Set runtime environment. + /// + /// Returns [`PubNubClientDeserializerBuilder`] where depending from enabled `features` following can be set: + /// * [`PubNub API`] response deserializer + /// * API ket set to access [`PubNub API`]. + /// + /// See [`Runtime`] trait for more information. + /// + /// # Examples + /// ``` + /// # use pubnub::core::Runtime; + /// use pubnub::{Keyset, PubNubClientBuilder}; + /// # use std::future::Future; + /// # + /// # #[derive(Clone)] + /// # struct MyRuntime; + /// # + /// # #[async_trait::async_trait] + /// # impl Runtime for MyRuntime { + /// # fn spawn(&self, future: impl Future + Send + 'static) { + /// # // spawn the Future + /// # // e.g. tokio::spawn(future); + /// # } + /// # + /// # async fn sleep(self, _delay: u64) { + /// # // e.g. tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await + /// # } + /// # } + /// + /// # fn main() -> Result<(), Box> { + /// // note that with_reqwest_transport is only available when + /// // the `reqwest` feature is enabled (default) + /// let client = PubNubClientBuilder::with_reqwest_transport() + /// .with_runtime(MyRuntime) + /// .with_keyset(Keyset { + /// subscribe_key: "sub-c-abc123", + /// publish_key: Some("pub-c-abc123"), + /// secret_key: None, + /// }) + /// .with_user_id("user-123") + /// .build(); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNubClientDeserializerBuilder`]: struct.PubNubClientDeserializerBuilder.html + /// [`Runtime`]: trait.Runtime.html + /// [`PubNub API`]: https://www.pubnub.com/docs + #[cfg(all(not(feature = "serde"), not(feature = "tokio")))] + pub fn with_runtime(self, runtime: R) -> PubNubClientDeserializerBuilder + where + R: Runtime + Send + Sync + 'static, + { + PubNubClientDeserializerBuilder { + transport: self.transport, + runtime: RuntimeSupport::new(Arc::new(runtime)), + } + } + + /// Set runtime environment. + /// + /// It returns [`PubNubClientKeySetBuilder`] builder that you can use + /// to set API ket set to access [`PubNub API`]. + /// + /// See [`Runtime`] trait for more information. + /// + /// # Examples + /// ``` + /// # use pubnub::core::Runtime; + /// use pubnub::{Keyset, PubNubClientBuilder}; + /// # use std::future::Future; + /// # + /// # #[derive(Clone)] + /// # struct MyRuntime; + /// # + /// # #[async_trait::async_trait] + /// # impl Runtime for MyRuntime { + /// # fn spawn(&self, future: impl Future + Send + 'static) { + /// # // spawn the Future + /// # // e.g. tokio::spawn(future); + /// # } + /// # + /// # async fn sleep(self, _delay: u64) { + /// # // e.g. tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await + /// # } + /// # } + /// + /// # fn main() -> Result<(), Box> { + /// // note that with_reqwest_transport is only available when + /// // the `reqwest` feature is enabled (default) + /// let client = PubNubClientBuilder::with_reqwest_transport() + /// .with_runtime(MyRuntime) + /// .with_keyset(Keyset { + /// subscribe_key: "sub-c-abc123", + /// publish_key: Some("pub-c-abc123"), + /// secret_key: None, + /// }) + /// .with_user_id("user-123") + /// .build(); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNubClientKeySetBuilder`]: struct.PubNubClientKeySetBuilder.html + /// [`Runtime`]: trait.Runtime.html + /// [`PubNub API`]: https://www.pubnub.com/docs + #[cfg(all(feature = "serde", not(feature = "tokio")))] + pub fn with_runtime(self, runtime: R) -> PubNubClientKeySetBuilder + where + R: Runtime + Send + Sync + 'static, + { + PubNubClientKeySetBuilder { + transport: self.transport, + deserializer: DeserializerSerde, + runtime: RuntimeSupport::new(Arc::new(runtime)), + } + } + + /// Set the keyset for the client. + /// + /// It returns [`PubNubClientUserIdBuilder`] builder that you can use + /// to set User ID for the client. + /// + /// See [`Keyset`] for more information. + /// + /// # Examples + /// ``` + /// use pubnub::{Keyset, PubNubClientBuilder}; + /// + /// # fn main() -> Result<(), Box> { + /// // note that with_reqwest_transport is only available when + /// // the `reqwest` feature is enabled (default) + /// let builder = PubNubClientBuilder::with_reqwest_transport() + /// .with_keyset(Keyset { + /// subscribe_key: "sub-c-abc123", + /// publish_key: Some("pub-c-abc123"), + /// secret_key: None, + /// }); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNubClientUserIdBuilder`]: struct.PubNubClientUserIdBuilder.html + /// [`Keyset`]: struct.Keyset.html + #[cfg(all(feature = "serde", feature = "tokio"))] + pub fn with_keyset( + self, + keyset: Keyset, + ) -> PubNubClientUserIdBuilder + where + S: Into, + { + PubNubClientUserIdBuilder { + transport: self.transport, + deserializer: DeserializerSerde, + keyset, + runtime: RuntimeSupport::new(Arc::new(RuntimeTokio)), + } + } +} + +/// PubNub builder for [`PubNubClient`] used to set custom deserializer. +/// +/// Deserializer will be used to process request responses from [`PubNub API`]. +/// +/// See [`PubNubClientDeserializerBuilder`] for more information. +/// +/// [`reqwest`]: https://docs.rs/reqwest +/// [`PubNub API`]: https://www.pubnub.com/docs +pub struct PubNubClientDeserializerBuilder { + /// Transport later. + pub(crate) transport: T, + + /// Runtime environment + #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] + pub(crate) runtime: RuntimeSupport, +} + +impl PubNubClientDeserializerBuilder { + /// Set [`PubNub API`] responses deserializer. + /// + /// It returns [`PubNubClientKeySetBuilder`] builder that you can use + /// to set API ket set to access [`PubNub API`]. + /// + /// See [`Deserializer`] for more information. + /// + /// + /// # Examples + /// ``` + /// # use pubnub::{ + /// # core::{Deserializer, PubNubError}, + /// # dx::presence::{result::HeartbeatResponseBody, HeartbeatResult}, + /// # }; + /// use pubnub::{Keyset, PubNubClientBuilder}; + /// # + /// # struct MyDeserializer; + /// # + /// # impl Deserializer for MyDeserializer { + /// # fn deserialize( + /// # &self, response: &[u8] + /// # ) -> Result { + /// # // ... + /// # Ok(HeartbeatResult) + /// # } + /// # } + /// + /// # fn main() -> Result<(), Box> { + /// // note that with_reqwest_transport is only available when + /// // the `reqwest` feature is enabled (default) + /// let builder = PubNubClientBuilder::with_reqwest_transport() + /// .with_deserializer(MyDeserializer) + /// .with_keyset(Keyset { + /// subscribe_key: "sub-c-abc123", + /// publish_key: Some("pub-c-abc123"), + /// secret_key: None, + /// }); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNubClientRuntimeBuilder`]: struct.PubNubClientRuntimeBuilder.html + /// [`Deserializer`]: trait.Deserializer.html + /// [`PubNub API`]: https://www.pubnub.com/docs + #[cfg(not(feature = "serde"))] + pub fn with_deserializer(self, deserializer: D) -> PubNubClientKeySetBuilder + where + D: Deserializer, + { + PubNubClientKeySetBuilder { + transport: self.transport, + deserializer, + #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] + runtime: self.runtime, + } + } + + /// Set the keyset for the client. + /// + /// It returns [`PubNubClientUserIdBuilder`] builder that you can use + /// to set User ID for the client. + /// + /// See [`Keyset`] for more information. + /// + /// # Examples + /// ``` + /// use pubnub::{Keyset, PubNubClientBuilder}; + /// + /// # fn main() -> Result<(), Box> { + /// // note that with_reqwest_transport is only available when + /// // the `reqwest` feature is enabled (default) + /// let builder = PubNubClientBuilder::with_reqwest_transport() + /// .with_keyset(Keyset { + /// subscribe_key: "sub-c-abc123", + /// publish_key: Some("pub-c-abc123"), + /// secret_key: None, + /// }); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`PubNubClientUserIdBuilder`]: struct.PubNubClientUserIdBuilder.html + /// [`Keyset`]: struct.Keyset.html + #[cfg(feature = "serde")] + pub fn with_keyset( + self, + keyset: Keyset, + ) -> PubNubClientUserIdBuilder + where + S: Into, + { + PubNubClientUserIdBuilder { + transport: self.transport, + deserializer: DeserializerSerde, + keyset, + #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] + runtime: self.runtime, } } } @@ -697,16 +1180,19 @@ impl PubNubClientBuilder { /// [`PubNubClient`] /// /// [`PubNubClientBuilder::with_keyset`]: struct.PubNubClientBuilder.html#method.with_keyset -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct PubNubClientUserIdBuilder +#[derive(Debug, Clone)] +pub struct PubNubClientUserIdBuilder where S: Into, { - transport: Option, + transport: T, + deserializer: D, keyset: Keyset, + #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] + runtime: RuntimeSupport, } -impl PubNubClientUserIdBuilder +impl PubNubClientUserIdBuilder where S: Into, { @@ -716,7 +1202,7 @@ where /// the PubNubClientConfigBuilder. /// /// [`PubNubClientConfigBuilder`]: struct.PubNubClientConfigBuilder.html - pub fn with_user_id(self, user_id: U) -> PubNubClientConfigBuilder + pub fn with_user_id(self, user_id: U) -> PubNubClientConfigBuilder where U: Into, { @@ -724,7 +1210,7 @@ where let secret_key = self.keyset.secret_key.map(|k| k.into()); PubNubClientConfigBuilder { - transport: self.transport, + transport: Some(self.transport), config: Some(PubNubConfig { publish_key, subscribe_key: self.keyset.subscribe_key.into(), @@ -733,7 +1219,14 @@ where auth_key: None, #[cfg(feature = "std")] retry_policy: Default::default(), + #[cfg(any(feature = "subscribe", feature = "presence"))] + heartbeat_value: 300, + #[cfg(any(feature = "subscribe", feature = "presence"))] + heartbeat_interval: None, }), + #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] + runtime: Some(self.runtime), + deserializer: Some(Arc::new(self.deserializer)), ..Default::default() } } @@ -771,7 +1264,6 @@ where mod should { use super::*; use crate::core::{TransportRequest, TransportResponse}; - use crate::lib::alloc::boxed::Box; use std::any::type_name; #[test] @@ -780,7 +1272,7 @@ mod should { struct MockTransport; #[async_trait::async_trait] - impl Transport for MockTransport { + impl crate::core::Transport for MockTransport { async fn send( &self, _request: TransportRequest, @@ -819,6 +1311,10 @@ mod should { auth_key: None, #[cfg(feature = "std")] retry_policy: Default::default(), + #[cfg(any(feature = "subscribe", feature = "presence"))] + heartbeat_value: 300, + #[cfg(any(feature = "subscribe", feature = "presence"))] + heartbeat_interval: None, }; assert!(config.signature_key_set().is_err()); diff --git a/src/dx/subscribe/builders/mod.rs b/src/dx/subscribe/builders/mod.rs index 34c5717c..04f5c226 100644 --- a/src/dx/subscribe/builders/mod.rs +++ b/src/dx/subscribe/builders/mod.rs @@ -1,29 +1,31 @@ -//! Subscribe builders module. - -use crate::{dx::pubnub_client::PubNubClientInstance, lib::alloc::string::String}; +//! # Subscribe API builders module. +//! +//! Module provides set of builders to work with [`PubNub`] subscribe API: +//! [`SubscribeRequestBuilder`] and [`SubscriptionBuilder`]. +//! +//! [`PubNub`]: https://www.pubnub.com #[doc(inline)] pub(crate) use subscribe::SubscribeRequestBuilder; pub(crate) mod subscribe; -#[cfg(all(not(feature = "serde"), feature = "std"))] -#[doc(inline)] -pub(crate) use subscription::SubscriptionWithDeserializerBuilder; - #[cfg(feature = "std")] #[doc(inline)] pub use subscription::{SubscriptionBuilder, SubscriptionBuilderError}; + #[cfg(feature = "std")] pub mod subscription; pub mod raw; -/// Validate [`PubNubClientInstance`] configuration. +use crate::{dx::pubnub_client::PubNubClientInstance, lib::alloc::string::String}; + +/// Validate [`PubNubClient`] configuration. /// /// Check whether if the [`PubNubConfig`] contains all the required fields set -/// for subscribe / unsubscribe endpoint usage or not. -pub(in crate::dx::subscribe::builders) fn validate_configuration( - client: &Option>, +/// for subscribe endpoint usage or not. +pub(in crate::dx::subscribe::builders) fn validate_configuration( + client: &Option>, ) -> Result<(), String> { if let Some(client) = client { if client.config.subscribe_key.is_empty() { diff --git a/src/dx/subscribe/builders/raw.rs b/src/dx/subscribe/builders/raw.rs index 3ef9e840..03f6b4db 100644 --- a/src/dx/subscribe/builders/raw.rs +++ b/src/dx/subscribe/builders/raw.rs @@ -1,7 +1,7 @@ //! # PubNub raw subscribe module. //! -//! This module has all the builders for raw subscription to real-time updates from -//! a list of channels and channel groups. +//! This module has all the builders for raw subscription to real-time updates +//! from a list of channels and channel groups. //! //! Raw subscription means that subscription will not perform any additional //! actions than minimal required to receive real-time updates. @@ -12,15 +12,16 @@ //! This one is used only for special cases when you need to have full control //! over subscription process or you need more compact subscription solution. +use derive_builder::Builder; + use crate::{ core::{blocking, Deserializer, PubNubError, Transport}, dx::{ pubnub_client::PubNubClientInstance, - subscribe::{SubscribeCursor, SubscribeResponseBody, Update}, + subscribe::{SubscribeCursor, Update}, }, - lib::alloc::{collections::VecDeque, string::String, string::ToString, sync::Arc, vec::Vec}, + lib::alloc::{collections::VecDeque, string::String, string::ToString, vec::Vec}, }; -use derive_builder::Builder; /// Raw subscription that is responsible for getting messages from PubNub. /// @@ -40,15 +41,12 @@ use derive_builder::Builder; build_fn(private, name = "build_internal", validate = "Self::validate"), no_std )] -pub struct RawSubscription -where - D: Deserializer, -{ +pub struct RawSubscription { /// Current client which can provide transportation to perform the request. /// /// This field is used to get [`Transport`] to perform the request. #[builder(field(vis = "pub(in crate::dx::subscribe)"), setter(custom))] - pub(in crate::dx::subscribe) pubnub_client: PubNubClientInstance, + pub(in crate::dx::subscribe) pubnub_client: PubNubClientInstance, /// Channels from which real-time updates should be received. /// @@ -83,17 +81,15 @@ where )] pub(in crate::dx::subscribe) cursor: Option, - /// Deserializer. + /// `user_id`presence timeout period. /// - /// Deserializer which will be used to deserialize received messages - /// from PubNub. - #[builder(field(vis = "pub(in crate::dx::subscribe)"), setter(custom))] - pub(in crate::dx::subscribe) deserializer: Arc, - - /// Heartbeat interval. + /// A heartbeat is a period of time during which `user_id` is visible + /// `online`. + /// If, within the heartbeat period, another heartbeat request or a + /// subscribe (for an implicit heartbeat) request `timeout` will be + /// announced for `user_id`. /// - /// Interval in seconds that informs the server that the client should - /// be considered alive. + /// By default it is set to **300** seconds. #[builder( field(vis = "pub(in crate::dx::subscribe)"), setter(strip_option), @@ -101,10 +97,14 @@ where )] pub(in crate::dx::subscribe) heartbeat: Option, - /// Expression used to filter received messages. + /// Message filtering predicate. /// - /// Expression used to filter received messages before they are delivered - /// to the client. + /// The [`PubNub`] network can filter out messages published with `meta` + /// before they reach subscribers using these filtering expressions, which + /// are based on the definition of the [`filter language`]. + /// + /// [`PubNub`]:https://www.pubnub.com/ + /// [`filter language`]: https://www.pubnub.com/docs/general/messages/publish#filter-language-definition #[builder( field(vis = "pub(in crate::dx::subscribe)"), setter(strip_option, into), @@ -113,35 +113,11 @@ where pub(in crate::dx::subscribe) filter_expression: Option, } -/// [`RawSubscriptionWithDeserializerBuilder`] is used to configure a subscription -/// listener with a custom deserializer. -pub struct RawSubscriptionWithDeserializerBuilder { - /// Subscription module configuration. - pub(in crate::dx::subscribe) client: PubNubClientInstance, -} - -impl RawSubscriptionWithDeserializerBuilder { - /// Create a new [`RawSubscriptionWithDeserializerBuilder`] instance. - pub fn deserialize_with(self, deserializer: D) -> RawSubscriptionBuilder - where - D: Deserializer, - { - RawSubscriptionBuilder { - pubnub_client: Some(self.client), - deserializer: Some(Arc::new(deserializer)), - ..Default::default() - } - } -} - -impl RawSubscriptionBuilder -where - D: Deserializer, -{ +impl RawSubscriptionBuilder { /// Validate user-provided data for request builder. /// /// Validator ensure that list of provided data is enough to build valid - /// request instance. + /// subscribe request instance. fn validate(&self) -> Result<(), String> { let groups_len = self.channel_groups.as_ref().map_or_else(|| 0, |v| v.len()); let channels_len = self.channels.as_ref().map_or_else(|| 0, |v| v.len()); @@ -154,10 +130,10 @@ where } } -impl RawSubscriptionBuilder +impl RawSubscriptionBuilder where - D: Deserializer, T: Transport, + D: Deserializer, { /// Build [`RawSubscription`] instance. /// @@ -166,7 +142,7 @@ where /// /// It creates a subscription object that can be used to get messages from /// PubNub. - pub fn execute(self) -> Result, PubNubError> { + pub fn execute(self) -> Result, PubNubError> { self.build_internal() .map_err(|e| PubNubError::SubscribeInitialization { details: e.to_string(), @@ -174,9 +150,8 @@ where } } -impl RawSubscriptionBuilder +impl RawSubscriptionBuilder where - D: Deserializer, T: blocking::Transport, { /// Build [`RawSubscription`] instance. @@ -186,7 +161,7 @@ where /// /// It creates a subscription object that can be used to get messages from /// PubNub. - pub fn execute_blocking(self) -> Result, PubNubError> { + pub fn execute_blocking(self) -> Result, PubNubError> { self.build_internal() .map_err(|e| PubNubError::SubscribeInitialization { details: e.to_string(), @@ -194,10 +169,10 @@ where } } -impl RawSubscription +impl RawSubscription where - D: Deserializer, T: Transport + 'static, + D: Deserializer + 'static, { /// Creates subscription stream. /// @@ -238,9 +213,7 @@ where request = request.filter_expression(filter_expr); } - let deserializer = ctx.subscription.deserializer.clone(); - - let response = request.execute(deserializer).await; + let response = request.execute().await; if let Err(e) = response { return Some(( @@ -260,9 +233,8 @@ where } } -impl RawSubscription +impl RawSubscription where - D: Deserializer, T: blocking::Transport, { /// Creates subscription iterator. @@ -271,7 +243,7 @@ where /// blocking iterator over messages received from PubNub. /// /// It loops the subscribe calls and iterator over messages from PubNub. - pub fn iter(self) -> RawSubscriptionIter { + pub fn iter(self) -> RawSubscriptionIter { let cursor = self .cursor .map(|tt| SubscribeCursor { @@ -290,10 +262,10 @@ where } } -impl Iterator for RawSubscriptionIter +impl Iterator for RawSubscriptionIter where - D: Deserializer, T: blocking::Transport, + D: Deserializer + 'static, { type Item = Result; @@ -317,9 +289,7 @@ where request = request.filter_expression(filter_expr); } - let deserializer = ctx.subscription.deserializer.clone(); - - let response = request.execute_blocking(deserializer); + let response = request.execute_blocking(); if let Err(e) = response { return Some(Err(PubNubError::general_api_error( @@ -350,11 +320,8 @@ where } } -struct SubscriptionContext -where - D: Deserializer, -{ - subscription: RawSubscription, +struct SubscriptionContext { + subscription: RawSubscription, cursor: SubscribeCursor, messages: VecDeque>, } @@ -364,33 +331,18 @@ where /// This iterator is returned by [`RawSubscription::iter`] method. /// It loops the subscribe calls and iterator over messages from PubNub. /// It can be used to get messages from PubNub. -pub struct RawSubscriptionIter(SubscriptionContext) -where - D: Deserializer; +pub struct RawSubscriptionIter(SubscriptionContext); #[cfg(test)] mod should { use super::*; use crate::{ - core::{ - blocking, Deserializer, PubNubError, Transport, TransportRequest, TransportResponse, - }, - dx::subscribe::{result::APISuccessBody, SubscribeResponseBody}, + core::{blocking, PubNubError, Transport, TransportRequest, TransportResponse}, + providers::deserialization_serde::DeserializerSerde, transport::middleware::PubNubMiddleware, Keyset, PubNubClientBuilder, }; - struct MockDeserializer; - - impl Deserializer for MockDeserializer { - fn deserialize(&self, _body: &[u8]) -> Result { - Ok(SubscribeResponseBody::SuccessResponse(APISuccessBody { - cursor: Default::default(), - messages: Default::default(), - })) - } - } - struct MockTransport; #[async_trait::async_trait] @@ -410,7 +362,7 @@ mod should { } } - fn client() -> PubNubClientInstance> { + fn client() -> PubNubClientInstance, DeserializerSerde> { PubNubClientBuilder::with_transport(MockTransport) .with_keyset(Keyset { subscribe_key: "demo", @@ -422,10 +374,9 @@ mod should { .unwrap() } - fn sut() -> RawSubscriptionBuilder> { + fn sut() -> RawSubscriptionBuilder, DeserializerSerde> { RawSubscriptionBuilder { pubnub_client: Some(client()), - deserializer: Some(Arc::new(MockDeserializer)), ..Default::default() } } diff --git a/src/dx/subscribe/builders/subscribe.rs b/src/dx/subscribe/builders/subscribe.rs index aa22ea60..8378ef9b 100644 --- a/src/dx/subscribe/builders/subscribe.rs +++ b/src/dx/subscribe/builders/subscribe.rs @@ -1,37 +1,40 @@ //! # PubNub subscribe module. //! -//! This module has all the builders for subscription to real-time updates from -//! a list of channels and channel groups. +//! The [`SubscribeRequestBuilder`] lets you to make and execute request that +//! will receive real-time updates from a list of channels and channel groups. +use derive_builder::Builder; #[cfg(feature = "std")] -use crate::dx::subscribe::cancel::CancellationTask; -use crate::dx::subscribe::SubscribeResponseBody; +use futures::{ + future::BoxFuture, + {select_biased, FutureExt}, +}; + use crate::{ core::{ blocking, - utils::encoding::join_url_encoded, + utils::encoding::{ + url_encode_extended, url_encoded_channel_groups, url_encoded_channels, + UrlEncodeExtension, + }, Deserializer, PubNubError, Transport, {TransportMethod, TransportRequest}, }, dx::{ pubnub_client::PubNubClientInstance, - subscribe::{builders, result::SubscribeResult, SubscribeCursor}, + subscribe::{builders, result::SubscribeResult, SubscribeCursor, SubscribeResponseBody}, }, lib::{ alloc::{ - boxed::Box, format, string::{String, ToString}, - sync::Arc, vec::Vec, }, collections::HashMap, }, }; -use derive_builder::Builder; -#[cfg(feature = "std")] -use futures::future::BoxFuture; + #[cfg(feature = "std")] -use futures::{select_biased, FutureExt}; +use crate::{core::event_engine::cancel::CancellationTask, lib::alloc::sync::Arc}; /// The [`SubscribeRequestBuilder`] is used to build subscribe request which /// will be used for real-time updates notification from the [`PubNub`] network. @@ -47,10 +50,12 @@ use futures::{select_biased, FutureExt}; build_fn(vis = "pub(in crate::dx::subscribe)", validate = "Self::validate"), no_std )] -pub(crate) struct SubscribeRequest { +pub(crate) struct SubscribeRequest { /// Current client which can provide transportation to perform the request. + /// + /// This field is used to get [`Transport`] to perform the request. #[builder(field(vis = "pub(in crate::dx::subscribe)"), setter(custom))] - pub(in crate::dx::subscribe) pubnub_client: PubNubClientInstance, + pub(in crate::dx::subscribe) pubnub_client: PubNubClientInstance, /// Channels from which real-time updates should be received. /// @@ -81,6 +86,15 @@ pub(crate) struct SubscribeRequest { )] pub(in crate::dx::subscribe) cursor: SubscribeCursor, + /// `user_id`presence timeout period. + /// + /// A heartbeat is a period of time during which `user_id` is visible + /// `online`. + /// If, within the heartbeat period, another heartbeat request or a + /// subscribe (for an implicit heartbeat) request `timeout` will be + /// announced for `user_id`. + /// + /// By default it is set to **300** seconds. #[builder( field(vis = "pub(in crate::dx::subscribe)"), setter(strip_option), @@ -88,6 +102,14 @@ pub(crate) struct SubscribeRequest { )] pub(in crate::dx::subscribe) heartbeat: u32, + /// Message filtering predicate. + /// + /// The [`PubNub`] network can filter out messages published with `meta` + /// before they reach subscribers using these filtering expressions, which + /// are based on the definition of the [`filter language`]. + /// + /// [`PubNub`]:https://www.pubnub.com/ + /// [`filter language`]: https://www.pubnub.com/docs/general/messages/publish#filter-language-definition #[builder( field(vis = "pub(in crate::dx::subscribe)"), setter(strip_option), @@ -96,43 +118,59 @@ pub(crate) struct SubscribeRequest { pub(in crate::dx::subscribe) filter_expression: Option, } -impl SubscribeRequest { +impl SubscribeRequestBuilder { + /// Validate user-provided data for request builder. + /// + /// Validator ensure that list of provided data is enough to build valid + /// subscribe request instance. + fn validate(&self) -> Result<(), String> { + let groups_len = self.channel_groups.as_ref().map_or_else(|| 0, |v| v.len()); + let channels_len = self.channels.as_ref().map_or_else(|| 0, |v| v.len()); + + builders::validate_configuration(&self.pubnub_client).and_then(|_| { + if channels_len == groups_len && channels_len == 0 { + Err("Either channels or channel groups should be provided".into()) + } else { + Ok(()) + } + }) + } + + /// Build [`HeartbeatRequest`] from builder. + fn request(self) -> Result, PubNubError> { + self.build() + .map_err(|err| PubNubError::general_api_error(err.to_string(), None, None)) + } +} + +impl SubscribeRequest { /// Create transport request from the request builder. pub(in crate::dx::subscribe) fn transport_request(&self) -> TransportRequest { let sub_key = &self.pubnub_client.config.subscribe_key; - let channels = join_url_encoded( - self.channels - .iter() - .map(|v| v.as_str()) - .collect::>() - .as_slice(), - ",", - ) - .unwrap_or(",".into()); let mut query: HashMap = HashMap::new(); query.extend::>(self.cursor.clone().into()); // Serialize list of channel groups and add into query parameters list. - join_url_encoded( - self.channel_groups - .iter() - .map(|v| v.as_str()) - .collect::>() - .as_slice(), - ",", - ) - .filter(|string| !string.is_empty()) - .and_then(|channel_groups| query.insert("channel-group".into(), channel_groups)); + url_encoded_channel_groups(&self.channel_groups) + .and_then(|groups| query.insert("channel-group".into(), groups)); self.filter_expression .as_ref() .filter(|e| !e.is_empty()) - .and_then(|e| query.insert("filter-expr".into(), e.into())); + .and_then(|e| { + query.insert( + "filter-expr".into(), + url_encode_extended(e.as_bytes(), UrlEncodeExtension::NonChannelPath), + ) + }); query.insert("heartbeat".into(), self.heartbeat.to_string()); TransportRequest { - path: format!("/v2/subscribe/{sub_key}/{channels}/0"), + path: format!( + "/v2/subscribe/{sub_key}/{}/0", + url_encoded_channels(&self.channels) + ), query_parameters: query, method: TransportMethod::Get, ..Default::default() @@ -140,126 +178,64 @@ impl SubscribeRequest { } } -impl SubscribeRequestBuilder { - /// Validate user-provided data for request builder. - /// - /// Validator ensure that list of provided data is enough to build valid - /// request instance. - fn validate(&self) -> Result<(), String> { - let groups_len = self.channel_groups.as_ref().map_or_else(|| 0, |v| v.len()); - let channels_len = self.channels.as_ref().map_or_else(|| 0, |v| v.len()); - - builders::validate_configuration(&self.pubnub_client).and_then(|_| { - if channels_len == groups_len && channels_len == 0 { - Err("Either channels or channel groups should be provided".into()) - } else { - Ok(()) - } - }) - } -} - -#[cfg(feature = "std")] -impl SubscribeRequestBuilder +impl SubscribeRequestBuilder where T: Transport, + D: Deserializer + 'static, { - pub async fn execute_with_cancel_and_delay( + /// Build and call asynchronous request. + pub async fn execute(self) -> Result { + let request = self.request()?; + let transport_request = request.transport_request(); + let client = request.pubnub_client.clone(); + let deserializer = client.deserializer.clone(); + transport_request + .send::(&client.transport, deserializer) + .await + } + + /// Build and call asynchronous request after delay. + /// + /// Perform delayed request call with ability to cancel it before call. + #[cfg(feature = "std")] + pub async fn execute_with_cancel_and_delay( self, - deserializer: Arc, delay: Arc, cancel_task: CancellationTask, ) -> Result where - D: Deserializer + ?Sized, F: Fn() -> BoxFuture<'static, ()> + Send + Sync + 'static, { select_biased! { _ = cancel_task.wait_for_cancel().fuse() => { Err(PubNubError::EffectCanceled) }, - response = self.execute_with_delay(deserializer, delay).fuse() => { + response = self.execute_with_delay(delay).fuse() => { response } } } - pub async fn execute_with_delay( - self, - deserializer: Arc, - delay: Arc, - ) -> Result + /// Build and call asynchronous request after configured delay. + #[cfg(feature = "std")] + async fn execute_with_delay(self, delay: Arc) -> Result where - D: Deserializer + ?Sized, F: Fn() -> BoxFuture<'static, ()> + Send + Sync + 'static, { - // Postpone request execution if required. + // Postpone request execution. delay().await; - self.execute(deserializer).await + self.execute().await } } -impl SubscribeRequestBuilder -where - T: Transport, -{ - /// Build and call request. - pub async fn execute(self, deserializer: Arc) -> Result - where - D: Deserializer + ?Sized, - { - // Build request instance and report errors if any. - let request = self - .build() - .map_err(|err| PubNubError::general_api_error(err.to_string(), None, None))?; - - let transport_request = request.transport_request(); - let client = request.pubnub_client.clone(); - - // Request configured endpoint. - let response = client.transport.send(transport_request).await?; - response - .clone() - .body - .map(|bytes| { - let deserialize_result = deserializer.deserialize(&bytes); - if deserialize_result.is_err() && response.status >= 500 { - Err(PubNubError::general_api_error( - "Unexpected service response", - None, - Some(Box::new(response.clone())), - )) - } else { - deserialize_result - } - }) - .map_or( - Err(PubNubError::general_api_error( - "No body in the response!", - None, - Some(Box::new(response.clone())), - )), - |response_body| { - response_body.and_then::(|body| { - body.try_into().map_err(|response_error: PubNubError| { - response_error.attach_response(response) - }) - }) - }, - ) - } -} - -impl SubscribeRequestBuilder +impl SubscribeRequestBuilder where T: blocking::Transport, + D: Deserializer + 'static, { - /// Build and call request. - pub fn execute_blocking(self, deserializer: Arc) -> Result - where - D: Deserializer + ?Sized, - { + /// Build and call synchronous request. + pub fn execute_blocking(self) -> Result { // Build request instance and report errors if any. let request = self .build() @@ -267,38 +243,9 @@ where let transport_request = request.transport_request(); let client = request.pubnub_client.clone(); - - // Request configured endpoint. - let response = client.transport.send(transport_request)?; - response - .clone() - .body - .map(|bytes| { - let deserialize_result = deserializer.deserialize(&bytes); - if deserialize_result.is_err() && response.status >= 500 { - Err(PubNubError::general_api_error( - "Unexpected service response", - None, - Some(Box::new(response.clone())), - )) - } else { - deserialize_result - } - }) - .map_or( - Err(PubNubError::general_api_error( - "No body in the response!", - None, - Some(Box::new(response.clone())), - )), - |response_body| { - response_body.and_then::(|body| { - body.try_into().map_err(|response_error: PubNubError| { - response_error.attach_response(response) - }) - }) - }, - ) + let deserializer = client.deserializer.clone(); + transport_request + .send_blocking::(&client.transport, deserializer) } } @@ -306,10 +253,7 @@ where #[cfg(test)] mod should { use super::*; - use crate::{ - core::TransportResponse, providers::deserialization_serde::SerdeDeserializer, - PubNubClientBuilder, - }; + use crate::{core::TransportResponse, PubNubClientBuilder}; use futures::future::ready; #[tokio::test] @@ -342,11 +286,7 @@ mod should { .unwrap() .subscribe_request() .channels(vec!["test".into()]) - .execute_with_cancel_and_delay( - Arc::new(SerdeDeserializer), - Arc::new(|| ready(()).boxed()), - cancel_task, - ) + .execute_with_cancel_and_delay(Arc::new(|| ready(()).boxed()), cancel_task) .await; assert!(matches!(result, Err(PubNubError::EffectCanceled))); diff --git a/src/dx/subscribe/builders/subscription.rs b/src/dx/subscribe/builders/subscription.rs index 98dc8f9f..61295cce 100644 --- a/src/dx/subscribe/builders/subscription.rs +++ b/src/dx/subscribe/builders/subscription.rs @@ -4,12 +4,15 @@ //! PubNub. It is responsible for handshake and receiving messages. //! It is also responsible for delivering messages to the user. +use derive_builder::Builder; +use futures::Stream; +use spin::RwLock; +use uuid::Uuid; + +use crate::subscribe::event_engine::SubscribeInput; use crate::{ - core::{Deserializer, PubNubError}, - dx::subscribe::{ - result::Update, types::SubscribeStreamEvent, SubscribeResponseBody, SubscribeStatus, - SubscriptionConfiguration, - }, + core::PubNubError, + dx::subscribe::{result::Update, types::SubscribeStreamEvent, SubscribeStatus}, lib::{ alloc::{ collections::VecDeque, @@ -24,18 +27,17 @@ use crate::{ task::{Context, Poll, Waker}, }, }, + subscribe::SubscriptionManager, }; -use derive_builder::Builder; -use futures::Stream; -use spin::RwLock; -use uuid::Uuid; /// Subscription stream. /// /// Stream delivers changes in subscription status: /// * `connected` - client connected to real-time [`PubNub`] network. -/// * `disconnected` - client has been disconnected from real-time [`PubNub`] network. -/// * `connection error` - client was unable to subscribe to specified channels and groups +/// * `disconnected` - client has been disconnected from real-time [`PubNub`] +/// network. +/// * `connection error` - client was unable to subscribe to specified channels +/// and groups /// /// and regular messages / signals. /// @@ -71,8 +73,10 @@ impl Clone for SubscriptionStream { /// /// Stream delivers changes in subscription status: /// * `connected` - client connected to real-time [`PubNub`] network. -/// * `disconnected` - client has been disconnected from real-time [`PubNub`] network. -/// * `connection error` - client was unable to subscribe to specified channels and groups +/// * `disconnected` - client has been disconnected from real-time [`PubNub`] +/// network. +/// * `connection error` - client was unable to subscribe to specified channels +/// and groups /// /// and regular messages / signals. /// @@ -139,29 +143,19 @@ pub struct SubscriptionRef { field(vis = "pub(in crate::dx::subscribe)"), setter(custom, strip_option) )] - pub(in crate::dx::subscribe) subscription: Arc>>, + pub(in crate::dx::subscribe) subscription: Arc>>, - /// Channels from which real-time updates should be received. + /// User input with channels and groups. /// - /// List of channels on which [`PubNubClient`] will subscribe and notify - /// about received real-time updates. + /// Object contains list of channels and channel groups on which + /// [`PubNubClient`] will subscribe and notify about received real-time + /// updates. #[builder( field(vis = "pub(in crate::dx::subscribe)"), - setter(into, strip_option), - default = "Vec::new()" - )] - pub(in crate::dx::subscribe) channels: Vec, - - /// Channel groups from which real-time updates should be received. - /// - /// List of groups of channels on which [`PubNubClient`] will subscribe and - /// notify about received real-time updates. - #[builder( - field(vis = "pub(in crate::dx::subscribe)"), - setter(into, strip_option), - default = "Vec::new()" + setter(custom), + default = "SubscribeInput::new(&None, &None)" )] - pub(in crate::dx::subscribe) channel_groups: Vec, + pub(in crate::dx::subscribe) input: SubscribeInput, /// Time cursor. /// @@ -242,27 +236,57 @@ pub struct SubscriptionRef { pub(in crate::dx::subscribe) status_stream: RwLock>>, } -/// [`SubscriptionWithDeserializerBuilder`] is used to configure a subscription -/// listener with a custom deserializer. -pub struct SubscriptionWithDeserializerBuilder { - /// Subscription module configuration. - pub(in crate::dx::subscribe) subscription: Arc>>, -} - impl SubscriptionBuilder { /// Validate user-provided data for request builder. /// /// Validator ensure that list of provided data is enough to build valid /// request instance. fn validate(&self) -> Result<(), String> { - let groups_len = self.channel_groups.as_ref().map_or_else(|| 0, |v| v.len()); - let channels_len = self.channels.as_ref().map_or_else(|| 0, |v| v.len()); + let input = self + .input + .as_ref() + .unwrap_or_else(|| panic!("Subscription input should be set by default")); + + if input.is_empty { + return Err("Either channels or channel groups should be provided".into()); + } + + Ok(()) + } + /// Channels from which real-time updates should be received. + /// + /// List of channels on which [`PubNubClient`] will subscribe and notify + /// about received real-time updates. + pub fn channels(mut self, channels: L) -> Self + where + L: Into>, + { + let user_input = SubscribeInput::new(&Some(channels.into()), &None); + if let Some(input) = self.input { + self.input = Some(input + user_input) + } else { + self.input = Some(user_input); + } - if channels_len == groups_len && channels_len == 0 { - Err("Either channels or channel groups should be provided".into()) + self + } + + /// Channel groups from which real-time updates should be received. + /// + /// List of groups of channels on which [`PubNubClient`] will subscribe and + /// notify about received real-time updates. + pub fn channel_groups(mut self, channel_groups: L) -> Self + where + L: Into>, + { + let user_input = SubscribeInput::new(&None, &Some(channel_groups.into())); + if let Some(input) = self.input { + self.input = Some(input + user_input) } else { - Ok(()) + self.input = Some(user_input); } + + self } } @@ -275,7 +299,7 @@ impl SubscriptionBuilder { }) .map(|subscription| { if let Some(manager) = subscription.subscription.write().as_mut() { - manager.subscription_manager.register(subscription.clone()) + manager.register(subscription.clone()) } subscription }) @@ -285,38 +309,12 @@ impl SubscriptionBuilder { } } -impl SubscriptionWithDeserializerBuilder { - /// Add custom deserializer. - /// - /// Adds the deserializer to the [`SubscriptionBuilder`]. - /// - /// Instance of [`SubscriptionBuilder`] returned. - pub fn deserialize_with(self, deserializer: D) -> SubscriptionBuilder - where - D: Deserializer + 'static, - { - { - if let Some(subscription) = self.subscription.write().as_mut() { - subscription - .deserializer - .is_none() - .then(|| subscription.deserializer = Some(Arc::new(deserializer))); - } - } - - SubscriptionBuilder { - subscription: Some(self.subscription), - ..Default::default() - } - } -} - impl Debug for SubscriptionRef { fn fmt(&self, f: &mut Formatter<'_>) -> crate::lib::core::fmt::Result { write!( f, "Subscription {{ \nchannels: {:?}, \nchannel-groups: {:?}, \ncursor: {:?}, \nheartbeat: {:?}, \nfilter_expression: {:?}}}", - self.channels, self.channel_groups, self.cursor, self.heartbeat, self.filter_expression + self.input.channels(), self.input.channel_groups(), self.cursor, self.heartbeat, self.filter_expression ) } } @@ -332,7 +330,7 @@ impl Subscription { /// ``` pub async fn unsubscribe(self) { if let Some(manager) = self.subscription.write().as_mut() { - manager.subscription_manager.unregister(self.clone()) + manager.unregister(self.clone()) } } @@ -462,7 +460,7 @@ impl Subscription { if let Some(stream) = stream.clone() { let mut updates_slot = stream.updates.write(); let updates_len = updates_slot.len(); - updates_slot.extend(messages.into_iter()); + updates_slot.extend(messages); updates_slot .len() .ne(&updates_len) @@ -504,17 +502,15 @@ impl Subscription { } fn subscribed_for_update(&self, update: &Update) -> bool { - self.channels.contains(&update.channel()) - || update - .channel_group() - .is_some_and(|g| self.channel_groups.contains(&g)) + self.input.contains_channel(&update.channel()) + || self.input.contains_channel_group(&update.channel_group()) } } impl SubscriptionStream { fn new(updates: VecDeque) -> Self { let mut stream_updates = VecDeque::with_capacity(100); - stream_updates.extend(updates.into_iter()); + stream_updates.extend(updates); Self { inner: Arc::new(SubscriptionStreamRef { diff --git a/src/dx/subscribe/event_engine/effect_handler.rs b/src/dx/subscribe/event_engine/effect_handler.rs index 343c3c9d..8c100b71 100644 --- a/src/dx/subscribe/event_engine/effect_handler.rs +++ b/src/dx/subscribe/event_engine/effect_handler.rs @@ -36,8 +36,7 @@ pub(crate) struct SubscribeEffectHandler { } impl SubscribeEffectHandler { - /// Create subscribe event handler. - #[allow(dead_code)] + /// Create subscribe effect handler. pub fn new( subscribe_call: Arc, emit_status: Arc, @@ -45,7 +44,7 @@ impl SubscribeEffectHandler { retry_policy: RequestRetryPolicy, cancellation_channel: Sender, ) -> Self { - SubscribeEffectHandler { + Self { subscribe_call, emit_status, emit_messages, @@ -58,26 +57,21 @@ impl SubscribeEffectHandler { impl EffectHandler for SubscribeEffectHandler { fn create(&self, invocation: &SubscribeEffectInvocation) -> Option { match invocation { - SubscribeEffectInvocation::Handshake { - channels, - channel_groups, - cursor, - } => Some(SubscribeEffect::Handshake { - channels: channels.clone(), - channel_groups: channel_groups.clone(), - cursor: cursor.clone(), - executor: self.subscribe_call.clone(), - cancellation_channel: self.cancellation_channel.clone(), - }), + SubscribeEffectInvocation::Handshake { input, cursor } => { + Some(SubscribeEffect::Handshake { + input: input.clone(), + cursor: cursor.clone(), + executor: self.subscribe_call.clone(), + cancellation_channel: self.cancellation_channel.clone(), + }) + } SubscribeEffectInvocation::HandshakeReconnect { - channels, - channel_groups, + input, cursor, attempts, reason, } => Some(SubscribeEffect::HandshakeReconnect { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: input.clone(), cursor: cursor.clone(), attempts: *attempts, reason: reason.clone(), @@ -85,26 +79,21 @@ impl EffectHandler for SubscribeEffe executor: self.subscribe_call.clone(), cancellation_channel: self.cancellation_channel.clone(), }), - SubscribeEffectInvocation::Receive { - channels, - channel_groups, - cursor, - } => Some(SubscribeEffect::Receive { - channels: channels.clone(), - channel_groups: channel_groups.clone(), - cursor: cursor.clone(), - executor: self.subscribe_call.clone(), - cancellation_channel: self.cancellation_channel.clone(), - }), + SubscribeEffectInvocation::Receive { input, cursor } => { + Some(SubscribeEffect::Receive { + input: input.clone(), + cursor: cursor.clone(), + executor: self.subscribe_call.clone(), + cancellation_channel: self.cancellation_channel.clone(), + }) + } SubscribeEffectInvocation::ReceiveReconnect { - channels, - channel_groups, + input, cursor, attempts, reason, } => Some(SubscribeEffect::ReceiveReconnect { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: input.clone(), cursor: cursor.clone(), attempts: *attempts, reason: reason.clone(), diff --git a/src/dx/subscribe/event_engine/effects/emit_messagess.rs b/src/dx/subscribe/event_engine/effects/emit_messagess.rs deleted file mode 100644 index 8ab16b8b..00000000 --- a/src/dx/subscribe/event_engine/effects/emit_messagess.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::{ - dx::subscribe::{ - event_engine::{effects::EmitMessagesEffectExecutor, SubscribeEvent}, - result::Update, - }, - lib::alloc::{sync::Arc, vec, vec::Vec}, -}; -use log::info; - -pub(super) async fn execute( - updates: Vec, - executor: &Arc, -) -> Vec { - info!("Emit updates: {updates:?}"); - - executor(updates); - - vec![] -} - -#[cfg(test)] -mod should { - use super::*; - use crate::dx::subscribe::types::Message; - - #[tokio::test] - async fn emit_expected_status() { - let message = Message { - sender: Some("test-user".into()), - timestamp: 1234567890, - channel: "test".to_string(), - subscription: "test-group".to_string(), - data: vec![], - r#type: None, - space_id: None, - decryption_error: None, - }; - - let emit_message_function: Arc = Arc::new(|updates| { - let emitted_update = updates.first().expect("update should be passed"); - assert!(matches!(emitted_update, Update::Message(_))); - - if let Update::Message(message) = emitted_update { - assert_eq!(*message, message.clone()); - } - }); - - execute( - vec![Update::Message(message.clone())], - &emit_message_function, - ) - .await; - } -} diff --git a/src/dx/subscribe/event_engine/effects/handshake.rs b/src/dx/subscribe/event_engine/effects/handshake.rs index c8f74dc8..84ff5aa2 100644 --- a/src/dx/subscribe/event_engine/effects/handshake.rs +++ b/src/dx/subscribe/event_engine/effects/handshake.rs @@ -1,28 +1,27 @@ +use futures::TryFutureExt; +use log::info; + use crate::{ - dx::subscribe::{ - event_engine::{effects::SubscribeEffectExecutor, SubscribeEvent}, - SubscriptionParams, + dx::subscribe::event_engine::{ + effects::SubscribeEffectExecutor, SubscribeEvent, SubscribeInput, SubscriptionParams, }, - lib::alloc::{string::String, sync::Arc, vec, vec::Vec}, + lib::alloc::{sync::Arc, vec, vec::Vec}, }; -use futures::TryFutureExt; -use log::info; pub(super) async fn execute( - channels: &Option>, - channel_groups: &Option>, + input: &SubscribeInput, effect_id: &str, executor: &Arc, ) -> Vec { info!( "Handshake for\nchannels: {:?}\nchannel groups: {:?}", - channels.as_ref().unwrap_or(&Vec::new()), - channel_groups.as_ref().unwrap_or(&Vec::new()) + input.channels().unwrap_or(Vec::new()), + input.channel_groups().unwrap_or(Vec::new()) ); executor(SubscriptionParams { - channels, - channel_groups, + channels: &input.channels(), + channel_groups: &input.channel_groups(), cursor: None, attempt: 0, reason: None, @@ -68,8 +67,10 @@ mod should { }); let result = execute( - &Some(vec!["ch1".to_string()]), - &Some(vec!["cg1".to_string()]), + &SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["cg1".to_string()]), + ), "id", &mock_handshake_function, ) @@ -95,8 +96,10 @@ mod should { }); let result = execute( - &Some(vec!["ch1".to_string()]), - &Some(vec!["cg1".to_string()]), + &SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["cg1".to_string()]), + ), "id", &mock_handshake_function, ) diff --git a/src/dx/subscribe/event_engine/effects/handshake_reconnection.rs b/src/dx/subscribe/event_engine/effects/handshake_reconnection.rs index e6052c95..bc3c657a 100644 --- a/src/dx/subscribe/event_engine/effects/handshake_reconnection.rs +++ b/src/dx/subscribe/event_engine/effects/handshake_reconnection.rs @@ -1,17 +1,16 @@ +use futures::TryFutureExt; +use log::info; + use crate::{ core::{PubNubError, RequestRetryPolicy}, - dx::subscribe::{ - event_engine::{effects::SubscribeEffectExecutor, SubscribeEvent}, - SubscriptionParams, + dx::subscribe::event_engine::{ + effects::SubscribeEffectExecutor, SubscribeEvent, SubscribeInput, SubscriptionParams, }, - lib::alloc::{string::String, sync::Arc, vec, vec::Vec}, + lib::alloc::{sync::Arc, vec, vec::Vec}, }; -use futures::TryFutureExt; -use log::info; pub(super) async fn execute( - channels: &Option>, - channel_groups: &Option>, + input: &SubscribeInput, attempt: u8, reason: PubNubError, effect_id: &str, @@ -24,13 +23,13 @@ pub(super) async fn execute( info!( "Handshake reconnection for\nchannels: {:?}\nchannel groups: {:?}", - channels.as_ref().unwrap_or(&Vec::new()), - channel_groups.as_ref().unwrap_or(&Vec::new()), + input.channels().unwrap_or(Vec::new()), + input.channel_groups().unwrap_or(Vec::new()) ); executor(SubscriptionParams { - channels, - channel_groups, + channels: &input.channels(), + channel_groups: &input.channel_groups(), cursor: None, attempt, reason: Some(reason), @@ -85,8 +84,10 @@ mod should { }); let result = execute( - &Some(vec!["ch1".to_string()]), - &Some(vec!["cg1".to_string()]), + &SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["cg1".to_string()]), + ), 1, PubNubError::Transport { details: "test".into(), @@ -121,8 +122,10 @@ mod should { }); let result = execute( - &Some(vec!["ch1".to_string()]), - &Some(vec!["cg1".to_string()]), + &SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["cg1".to_string()]), + ), 1, PubNubError::Transport { details: "test".into(), diff --git a/src/dx/subscribe/event_engine/effects/mod.rs b/src/dx/subscribe/event_engine/effects/mod.rs index edf54ff2..90da1c5c 100644 --- a/src/dx/subscribe/event_engine/effects/mod.rs +++ b/src/dx/subscribe/event_engine/effects/mod.rs @@ -1,17 +1,23 @@ +//! # Subscribe event engine effect module. + +use async_channel::Sender; +use futures::future::BoxFuture; + use crate::{ core::{event_engine::Effect, PubNubError, RequestRetryPolicy}, dx::subscribe::{ - event_engine::{SubscribeEffectInvocation, SubscribeEvent}, + event_engine::{ + types::{SubscribeInput, SubscriptionParams}, + SubscribeEffectInvocation, SubscribeEvent, + }, result::{SubscribeResult, Update}, - SubscribeCursor, SubscribeStatus, SubscriptionParams, + SubscribeCursor, SubscribeStatus, }, lib::{ alloc::{boxed::Box, string::String, sync::Arc, vec::Vec}, core::fmt::{Debug, Formatter}, }, }; -use async_channel::Sender; -use futures::future::BoxFuture; mod emit_messages; mod emit_status; @@ -33,17 +39,11 @@ pub(in crate::dx::subscribe) type EmitMessagesEffectExecutor = dyn Fn(Vec>, - - /// Optional list of channel groups. - /// - /// List of channel groups which will be source of real-time updates - /// after initial subscription completion. - channel_groups: Option>, + /// Object contains list of channels and channel groups which will be + /// source of real-time updates after initial subscription completion. + input: SubscribeInput, /// Time cursor. /// @@ -64,17 +64,11 @@ pub(crate) enum SubscribeEffect { /// Retry initial subscribe effect invocation. HandshakeReconnect { - /// Optional list of channels. + /// User input with channels and groups. /// - /// List of channels which has been used during recently failed initial - /// subscription. - channels: Option>, - - /// Optional list of channel groups. - /// - /// List of channel groups which has been used during recently failed - /// initial subscription. - channel_groups: Option>, + /// Object contains list of channels and channel groups which has been + /// used during recently failed initial subscription. + input: SubscribeInput, /// Time cursor. /// @@ -106,16 +100,11 @@ pub(crate) enum SubscribeEffect { /// Receive updates effect invocation. Receive { - /// Optional list of channels. + /// User input with channels and groups. /// - /// List of channels for which real-time updates will be delivered. - channels: Option>, - - /// Optional list of channel groups. - /// - /// List of channel groups for which real-time updates will be - /// delivered. - channel_groups: Option>, + /// Object contains list of channels and channel groups for which + /// real-time updates will be delivered. + input: SubscribeInput, /// Time cursor. /// @@ -136,17 +125,11 @@ pub(crate) enum SubscribeEffect { /// Retry receive updates effect invocation. ReceiveReconnect { - /// Optional list of channels. - /// - /// List of channels which has been used during recently failed receive - /// updates. - channels: Option>, - - /// Optional list of channel groups. + /// User input with channels and groups. /// - /// List of channel groups which has been used during recently failed - /// receive updates. - channel_groups: Option>, + /// Object contains list of channels and channel groups which has been + /// used during recently failed receive updates. + input: SubscribeInput, /// Time cursor. /// @@ -202,51 +185,47 @@ pub(crate) enum SubscribeEffect { impl Debug for SubscribeEffect { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { match self { - SubscribeEffect::Handshake { - channels, - channel_groups, - .. - } => write!( + Self::Handshake { input, .. } => write!( f, - "SubscribeEffect::Handshake {{ channels: {channels:?}, channel groups: \ - {channel_groups:?} }}" + "SubscribeEffect::Handshake {{ channels: {:?}, channel groups: {:?} }}", + input.channels(), + input.channel_groups() ), - SubscribeEffect::HandshakeReconnect { - channels, - channel_groups, + Self::HandshakeReconnect { + input, attempts, reason, .. } => write!( f, - "SubscribeEffect::HandshakeReconnect {{ channels: {channels:?}, channel groups: \ - {channel_groups:?}, attempts: {attempts:?}, reason: {reason:?} }}" + "SubscribeEffect::HandshakeReconnect {{ channels: {:?}, channel groups: {:?}, \ + attempts: {attempts:?}, reason: {reason:?} }}", + input.channels(), + input.channel_groups() ), - SubscribeEffect::Receive { - channels, - channel_groups, - cursor, - .. - } => write!( + Self::Receive { input, cursor, .. } => write!( f, - "SubscribeEffect::Receive {{ channels: {channels:?}, channel groups: \ - {channel_groups:?}, cursor: {cursor:?} }}" + "SubscribeEffect::Receive {{ channels: {:?}, channel groups: {:?}, cursor: \ + {cursor:?} }}", + input.channels(), + input.channel_groups() ), - SubscribeEffect::ReceiveReconnect { - channels, - channel_groups, + Self::ReceiveReconnect { + input, attempts, reason, .. } => write!( f, - "SubscribeEffect::ReceiveReconnect {{ channels: {channels:?}, channel groups: \ - {channel_groups:?}, attempts: {attempts:?}, reason: {reason:?} }}" + "SubscribeEffect::ReceiveReconnect {{ channels: {:?}, channel groups: {:?}, \ + attempts: {attempts:?}, reason: {reason:?} }}", + input.channels(), + input.channel_groups() ), - SubscribeEffect::EmitStatus { status, .. } => { + Self::EmitStatus { status, .. } => { write!(f, "SubscribeEffect::EmitStatus {{ status: {status:?} }}") } - SubscribeEffect::EmitMessages { updates, .. } => { + Self::EmitMessages { updates, .. } => { write!( f, "SubscribeEffect::EmitMessages {{ messages: {updates:?} }}" @@ -262,26 +241,23 @@ impl Effect for SubscribeEffect { fn id(&self) -> String { match self { - SubscribeEffect::Handshake { .. } => "HANDSHAKE_EFFECT".into(), - SubscribeEffect::HandshakeReconnect { .. } => "HANDSHAKE_RECONNECT_EFFECT".into(), - SubscribeEffect::Receive { .. } => "RECEIVE_EFFECT".into(), - SubscribeEffect::ReceiveReconnect { .. } => "RECEIVE_RECONNECT_EFFECT".into(), - SubscribeEffect::EmitStatus { .. } => "EMIT_STATUS_EFFECT".into(), - SubscribeEffect::EmitMessages { .. } => "EMIT_MESSAGES_EFFECT".into(), + Self::Handshake { .. } => "HANDSHAKE_EFFECT", + Self::HandshakeReconnect { .. } => "HANDSHAKE_RECONNECT_EFFECT", + Self::Receive { .. } => "RECEIVE_EFFECT", + Self::ReceiveReconnect { .. } => "RECEIVE_RECONNECT_EFFECT", + Self::EmitStatus { .. } => "EMIT_STATUS_EFFECT", + Self::EmitMessages { .. } => "EMIT_MESSAGES_EFFECT", } + .into() } async fn run(&self) -> Vec { match self { - SubscribeEffect::Handshake { - channels, - channel_groups, - executor, - .. - } => handshake::execute(channels, channel_groups, &self.id(), executor).await, - SubscribeEffect::HandshakeReconnect { - channels, - channel_groups, + Self::Handshake { + input, executor, .. + } => handshake::execute(input, &self.id(), executor).await, + Self::HandshakeReconnect { + input, attempts, reason, retry_policy, @@ -289,8 +265,7 @@ impl Effect for SubscribeEffect { .. } => { handshake_reconnection::execute( - channels, - channel_groups, + input, *attempts, reason.clone(), /* TODO: Does run function need to borrow self? Or we can * consume it? */ @@ -300,16 +275,14 @@ impl Effect for SubscribeEffect { ) .await } - SubscribeEffect::Receive { - channels, - channel_groups, + Self::Receive { + input, cursor, executor, .. - } => receive::execute(channels, channel_groups, cursor, &self.id(), executor).await, - SubscribeEffect::ReceiveReconnect { - channels, - channel_groups, + } => receive::execute(input, cursor, &self.id(), executor).await, + Self::ReceiveReconnect { + input, cursor, attempts, reason, @@ -318,8 +291,7 @@ impl Effect for SubscribeEffect { .. } => { receive_reconnection::execute( - channels, - channel_groups, + input, cursor, *attempts, reason.clone(), /* TODO: Does run function need to borrow self? Or we can @@ -330,10 +302,10 @@ impl Effect for SubscribeEffect { ) .await } - SubscribeEffect::EmitStatus { status, executor } => { + Self::EmitStatus { status, executor } => { emit_status::execute(status.clone(), executor).await } - SubscribeEffect::EmitMessages { updates, executor } => { + Self::EmitMessages { updates, executor } => { emit_messages::execute(updates.clone(), executor).await } } @@ -341,19 +313,19 @@ impl Effect for SubscribeEffect { fn cancel(&self) { match self { - SubscribeEffect::Handshake { + Self::Handshake { cancellation_channel, .. } - | SubscribeEffect::HandshakeReconnect { + | Self::HandshakeReconnect { cancellation_channel, .. } - | SubscribeEffect::Receive { + | Self::Receive { cancellation_channel, .. } - | SubscribeEffect::ReceiveReconnect { + | Self::ReceiveReconnect { cancellation_channel, .. } => { @@ -375,8 +347,7 @@ mod should { let (tx, rx) = async_channel::bounded(1); let effect = SubscribeEffect::Handshake { - channels: None, - channel_groups: None, + input: SubscribeInput::new(&None, &None), cursor: None, executor: Arc::new(|_| { Box::pin(async move { diff --git a/src/dx/subscribe/event_engine/effects/receive.rs b/src/dx/subscribe/event_engine/effects/receive.rs index 18281f1e..97bf75db 100644 --- a/src/dx/subscribe/event_engine/effects/receive.rs +++ b/src/dx/subscribe/event_engine/effects/receive.rs @@ -1,16 +1,19 @@ +use futures::TryFutureExt; +use log::info; + use crate::{ dx::subscribe::{ - event_engine::{effects::SubscribeEffectExecutor, SubscribeEvent}, - SubscribeCursor, SubscriptionParams, + event_engine::{ + effects::SubscribeEffectExecutor, types::SubscriptionParams, SubscribeEvent, + SubscribeInput, + }, + SubscribeCursor, }, - lib::alloc::{string::String, sync::Arc, vec, vec::Vec}, + lib::alloc::{sync::Arc, vec, vec::Vec}, }; -use futures::TryFutureExt; -use log::info; pub(crate) async fn execute( - channels: &Option>, - channel_groups: &Option>, + input: &SubscribeInput, cursor: &SubscribeCursor, effect_id: &str, executor: &Arc, @@ -18,13 +21,13 @@ pub(crate) async fn execute( info!( "Receive at {:?} for\nchannels: {:?}\nchannel groups: {:?}", cursor.timetoken, - channels.as_ref().unwrap_or(&Vec::new()), - channel_groups.as_ref().unwrap_or(&Vec::new()), + input.channels().unwrap_or(Vec::new()), + input.channel_groups().unwrap_or(Vec::new()) ); executor(SubscriptionParams { - channels, - channel_groups, + channels: &input.channels(), + channel_groups: &input.channel_groups(), cursor: Some(cursor), attempt: 0, reason: None, @@ -71,8 +74,10 @@ mod should { }); let result = execute( - &Some(vec!["ch1".to_string()]), - &Some(vec!["cg1".to_string()]), + &SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["cg1".to_string()]), + ), &Default::default(), "id", &mock_receive_function, @@ -99,8 +104,10 @@ mod should { }); let result = execute( - &Some(vec!["ch1".to_string()]), - &Some(vec!["cg1".to_string()]), + &SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["cg1".to_string()]), + ), &Default::default(), "id", &mock_receive_function, diff --git a/src/dx/subscribe/event_engine/effects/receive_reconnection.rs b/src/dx/subscribe/event_engine/effects/receive_reconnection.rs index ad9bc343..f80f536d 100644 --- a/src/dx/subscribe/event_engine/effects/receive_reconnection.rs +++ b/src/dx/subscribe/event_engine/effects/receive_reconnection.rs @@ -1,18 +1,21 @@ +use futures::TryFutureExt; +use log::info; + use crate::{ core::{PubNubError, RequestRetryPolicy}, dx::subscribe::{ - event_engine::{effects::SubscribeEffectExecutor, SubscribeEvent}, - SubscribeCursor, SubscriptionParams, + event_engine::{ + effects::SubscribeEffectExecutor, types::SubscriptionParams, SubscribeEvent, + SubscribeInput, + }, + SubscribeCursor, }, - lib::alloc::{string::String, sync::Arc, vec, vec::Vec}, + lib::alloc::{sync::Arc, vec, vec::Vec}, }; -use futures::TryFutureExt; -use log::info; #[allow(clippy::too_many_arguments)] pub(crate) async fn execute( - channels: &Option>, - channel_groups: &Option>, + input: &SubscribeInput, cursor: &SubscribeCursor, attempt: u8, reason: PubNubError, @@ -27,13 +30,13 @@ pub(crate) async fn execute( info!( "Receive reconnection at {:?} for\nchannels: {:?}\nchannel groups: {:?}", cursor.timetoken, - channels.as_ref().unwrap_or(&Vec::new()), - channel_groups.as_ref().unwrap_or(&Vec::new()), + input.channels().unwrap_or(Vec::new()), + input.channel_groups().unwrap_or(Vec::new()) ); executor(SubscriptionParams { - channels, - channel_groups, + channels: &input.channels(), + channel_groups: &input.channel_groups(), cursor: Some(cursor), attempt, reason: Some(reason), @@ -96,8 +99,10 @@ mod should { }); let result = execute( - &Some(vec!["ch1".to_string()]), - &Some(vec!["cg1".to_string()]), + &SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["cg1".to_string()]), + ), &Default::default(), 10, PubNubError::Transport { @@ -136,8 +141,52 @@ mod should { }); let result = execute( - &Some(vec!["ch1".to_string()]), - &Some(vec!["cg1".to_string()]), + &SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["cg1".to_string()]), + ), + &Default::default(), + 5, + PubNubError::Transport { + details: "test".into(), + response: Some(Box::new(TransportResponse { + status: 500, + ..Default::default() + })), + }, + "id", + &RequestRetryPolicy::None, + &mock_receive_function, + ) + .await; + + assert!(!result.is_empty()); + assert!(matches!( + result.first().unwrap(), + SubscribeEvent::ReceiveReconnectFailure { .. } + )); + } + + #[tokio::test] + async fn return_receive_reconnect_give_up_event_on_err() { + let mock_receive_function: Arc = Arc::new(move |_| { + async move { + Err(PubNubError::Transport { + details: "test".into(), + response: Some(Box::new(TransportResponse { + status: 500, + ..Default::default() + })), + }) + } + .boxed() + }); + + let result = execute( + &SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["cg1".to_string()]), + ), &Default::default(), 10, PubNubError::Transport { diff --git a/src/dx/subscribe/event_engine/event.rs b/src/dx/subscribe/event_engine/event.rs index 3f54e505..ed61e2f9 100644 --- a/src/dx/subscribe/event_engine/event.rs +++ b/src/dx/subscribe/event_engine/event.rs @@ -59,7 +59,7 @@ pub(crate) enum SubscribeEvent { /// Handshake reconnect completed with error. /// /// Emitted when another handshake effect attempt was unable to receive - /// response from [`PubNub`] network (network issues or permissions). + /// response from [`PubNub`] network (network or permissions issues). /// /// [`PubNub`]: https://www.pubnub.com/ HandshakeReconnectFailure { reason: PubNubError }, @@ -142,21 +142,21 @@ pub(crate) enum SubscribeEvent { impl Event for SubscribeEvent { fn id(&self) -> &str { match self { - SubscribeEvent::SubscriptionChanged { .. } => "SUBSCRIPTION_CHANGED", - SubscribeEvent::SubscriptionRestored { .. } => "SUBSCRIPTION_RESTORED", - SubscribeEvent::HandshakeSuccess { .. } => "HANDSHAKE_SUCCESS", - SubscribeEvent::HandshakeFailure { .. } => "HANDSHAKE_FAILURE", - SubscribeEvent::HandshakeReconnectSuccess { .. } => "HANDSHAKE_RECONNECT_SUCCESS", - SubscribeEvent::HandshakeReconnectFailure { .. } => "HANDSHAKE_RECONNECT_FAILURE", - SubscribeEvent::HandshakeReconnectGiveUp { .. } => "HANDSHAKE_RECONNECT_GIVEUP", - SubscribeEvent::ReceiveSuccess { .. } => "RECEIVE_SUCCESS", - SubscribeEvent::ReceiveFailure { .. } => "RECEIVE_FAILURE", - SubscribeEvent::ReceiveReconnectSuccess { .. } => "RECEIVE_RECONNECT_SUCCESS", - SubscribeEvent::ReceiveReconnectFailure { .. } => "RECEIVE_RECONNECT_FAILURE", - SubscribeEvent::ReceiveReconnectGiveUp { .. } => "RECEIVE_RECONNECT_GIVEUP", - SubscribeEvent::Disconnect => "DISCONNECT", - SubscribeEvent::Reconnect => "RECONNECT", - SubscribeEvent::UnsubscribeAll => "UNSUBSCRIBE_ALL", + Self::SubscriptionChanged { .. } => "SUBSCRIPTION_CHANGED", + Self::SubscriptionRestored { .. } => "SUBSCRIPTION_RESTORED", + Self::HandshakeSuccess { .. } => "HANDSHAKE_SUCCESS", + Self::HandshakeFailure { .. } => "HANDSHAKE_FAILURE", + Self::HandshakeReconnectSuccess { .. } => "HANDSHAKE_RECONNECT_SUCCESS", + Self::HandshakeReconnectFailure { .. } => "HANDSHAKE_RECONNECT_FAILURE", + Self::HandshakeReconnectGiveUp { .. } => "HANDSHAKE_RECONNECT_GIVEUP", + Self::ReceiveSuccess { .. } => "RECEIVE_SUCCESS", + Self::ReceiveFailure { .. } => "RECEIVE_FAILURE", + Self::ReceiveReconnectSuccess { .. } => "RECEIVE_RECONNECT_SUCCESS", + Self::ReceiveReconnectFailure { .. } => "RECEIVE_RECONNECT_FAILURE", + Self::ReceiveReconnectGiveUp { .. } => "RECEIVE_RECONNECT_GIVEUP", + Self::Disconnect => "DISCONNECT", + Self::Reconnect => "RECONNECT", + Self::UnsubscribeAll => "UNSUBSCRIBE_ALL", } } } diff --git a/src/dx/subscribe/event_engine/invocation.rs b/src/dx/subscribe/event_engine/invocation.rs index e1b16c3d..7528c6d5 100644 --- a/src/dx/subscribe/event_engine/invocation.rs +++ b/src/dx/subscribe/event_engine/invocation.rs @@ -1,12 +1,12 @@ use crate::{ core::{event_engine::EffectInvocation, PubNubError}, dx::subscribe::{ - event_engine::{SubscribeEffect, SubscribeEvent}, + event_engine::{SubscribeEffect, SubscribeEvent, SubscribeInput}, result::Update, SubscribeCursor, SubscribeStatus, }, lib::{ - alloc::{string::String, vec::Vec}, + alloc::vec::Vec, core::fmt::{Display, Formatter, Result}, }, }; @@ -20,17 +20,11 @@ use crate::{ pub(crate) enum SubscribeEffectInvocation { /// Initial subscribe effect invocation. Handshake { - /// Optional list of channels. + /// User input with channels and groups. /// - /// List of channels which will be source of real-time updates after - /// initial subscription completion. - channels: Option>, - - /// Optional list of channel groups. - /// - /// List of channel groups which will be source of real-time updates - /// after initial subscription completion. - channel_groups: Option>, + /// Object contains list of channels and groups which will be source of + /// real-time updates after initial subscription completion. + input: SubscribeInput, /// Time cursor. /// @@ -44,17 +38,11 @@ pub(crate) enum SubscribeEffectInvocation { /// Retry initial subscribe effect invocation. HandshakeReconnect { - /// Optional list of channels. - /// - /// List of channels which has been used during recently failed initial - /// subscription. - channels: Option>, - - /// Optional list of channel groups. + /// User input with channels and groups. /// - /// List of channel groups which has been used during recently failed - /// initial subscription. - channel_groups: Option>, + /// Object contains list of channels and groups which has been used + /// during recently failed initial subscription. + input: SubscribeInput, /// Time cursor. /// @@ -76,16 +64,11 @@ pub(crate) enum SubscribeEffectInvocation { /// Receive updates effect invocation. Receive { - /// Optional list of channels. + /// User input with channels and groups. /// - /// List of channels for which real-time updates will be delivered. - channels: Option>, - - /// Optional list of channel groups. - /// - /// List of channel groups for which real-time updates will be - /// delivered. - channel_groups: Option>, + /// Object contains list of channels and groups which real-time updates + /// will be delivered. + input: SubscribeInput, /// Time cursor. /// @@ -99,17 +82,11 @@ pub(crate) enum SubscribeEffectInvocation { /// Retry receive updates effect invocation. ReceiveReconnect { - /// Optional list of channels. - /// - /// List of channels which has been used during recently failed receive - /// updates. - channels: Option>, - - /// Optional list of channel groups. + /// User input with channels and groups. /// - /// List of channel groups which has been used during recently failed - /// receive updates. - channel_groups: Option>, + /// Object contains list of channels and groups which has been used + /// during recently failed receive updates. + input: SubscribeInput, /// Time cursor. /// diff --git a/src/dx/subscribe/event_engine/mod.rs b/src/dx/subscribe/event_engine/mod.rs index a0146351..0e4dfdae 100644 --- a/src/dx/subscribe/event_engine/mod.rs +++ b/src/dx/subscribe/event_engine/mod.rs @@ -1,11 +1,15 @@ //! Subscribe Event Engine module +use crate::{ + core::event_engine::EventEngine, + lib::alloc::{string::String, vec::Vec}, +}; + #[doc(inline)] pub(crate) use effects::SubscribeEffect; pub(crate) mod effects; #[doc(inline)] -#[allow(unused_imports)] pub(crate) use effect_handler::SubscribeEffectHandler; pub(crate) mod effect_handler; @@ -22,10 +26,10 @@ pub(crate) mod event; pub(crate) use state::SubscribeState; pub(crate) mod state; -use crate::{ - core::event_engine::EventEngine, - lib::alloc::{string::String, vec::Vec}, -}; +#[doc(inline)] +#[allow(unused_imports)] +pub(in crate::dx::subscribe) use types::{SubscribeInput, SubscriptionParams}; +pub(in crate::dx::subscribe) mod types; pub(crate) type SubscribeEventEngine = EventEngine; @@ -38,46 +42,16 @@ impl &self, ) -> (Option>, Option>) { match self.current_state() { - SubscribeState::Handshaking { - channels, - channel_groups, - .. - } - | SubscribeState::HandshakeReconnecting { - channels, - channel_groups, - .. - } - | SubscribeState::HandshakeStopped { - channels, - channel_groups, - .. - } - | SubscribeState::HandshakeFailed { - channels, - channel_groups, - .. - } - | SubscribeState::Receiving { - channels, - channel_groups, - .. - } - | SubscribeState::ReceiveReconnecting { - channels, - channel_groups, - .. - } - | SubscribeState::ReceiveStopped { - channels, - channel_groups, - .. + SubscribeState::Handshaking { input, .. } + | SubscribeState::HandshakeReconnecting { input, .. } + | SubscribeState::HandshakeStopped { input, .. } + | SubscribeState::HandshakeFailed { input, .. } + | SubscribeState::Receiving { input, .. } + | SubscribeState::ReceiveReconnecting { input, .. } + | SubscribeState::ReceiveStopped { input, .. } + | SubscribeState::ReceiveFailed { input, .. } => { + (input.channels(), input.channel_groups()) } - | SubscribeState::ReceiveFailed { - channels, - channel_groups, - .. - } => (channels, channel_groups), _ => (None, None), } } diff --git a/src/dx/subscribe/event_engine/state.rs b/src/dx/subscribe/event_engine/state.rs index ace2ec52..a4070258 100644 --- a/src/dx/subscribe/event_engine/state.rs +++ b/src/dx/subscribe/event_engine/state.rs @@ -1,3 +1,9 @@ +//! # Heartbeat event engine state module. +//! +//! The module contains the [`SubscribeState`] type, which describes available +//! event engine states. The module also contains an implementation of +//! `transition` between states in response to certain events. + use crate::{ core::{ event_engine::{State, Transition}, @@ -5,6 +11,7 @@ use crate::{ }, dx::subscribe::{ event_engine::{ + types::SubscribeInput, SubscribeEffectInvocation::{ self, CancelHandshake, CancelHandshakeReconnect, CancelReceive, CancelReceiveReconnect, EmitMessages, EmitStatus, Handshake, HandshakeReconnect, @@ -33,22 +40,16 @@ pub(crate) enum SubscribeState { /// Retrieve the information that will be used to start the subscription /// loop. Handshaking { - /// Optional list of channels. - /// - /// List of channels which will be source of real-time updates after - /// initial subscription completion. - channels: Option>, - - /// Optional list of channel groups. + /// User input with channels and groups. /// - /// List of channel groups which will be source of real-time updates - /// after initial subscription completion. - channel_groups: Option>, + /// Object contains list of channels and groups which will be source of + /// real-time updates after initial subscription completion. + input: SubscribeInput, - /// Time cursor. + /// Custom time cursor. /// - /// Cursor used by subscription loop to identify point in time after - /// which updates will be delivered. + /// Custom cursor used by subscription loop to identify point in time + /// after which updates will be delivered. cursor: Option, }, @@ -56,22 +57,16 @@ pub(crate) enum SubscribeState { /// /// The system is recovering after the initial subscription attempt failed. HandshakeReconnecting { - /// Optional list of channels. - /// - /// List of channels which has been used during recently failed initial - /// subscription. - channels: Option>, - - /// Optional list of channel groups. + /// User input with channels and groups. /// - /// List of channel groups which has been used during recently failed - /// initial subscription. - channel_groups: Option>, + /// Object contains list of channels and groups which has been used + /// during recently failed initial subscription. + input: SubscribeInput, - /// Time cursor. + /// Custom time cursor. /// - /// Cursor used by subscription loop to identify point in time after - /// which updates will be delivered. + /// Custom cursor used by subscription loop to identify point in time + /// after which updates will be delivered. cursor: Option, /// Current initial subscribe retry attempt. @@ -84,26 +79,17 @@ pub(crate) enum SubscribeState { }, /// Initial subscription stopped state. - /// - /// Subscription state machine state, which is set when - /// [`SubscribeEvent::Disconnect`] event sent while in - /// [`SubscribeState::Handshaking`] or - /// [`SubscribeState::HandshakeReconnecting`] state. HandshakeStopped { - /// Optional list of channels. + /// User input with channels and groups. /// - /// List of channels for which initial subscription stopped. - channels: Option>, + /// Object contains list of channels and groups for which initial + /// subscription stopped. + input: SubscribeInput, - /// Optional list of channel groups. + /// Custom time cursor. /// - /// List of channel groups for which initial subscription stopped. - channel_groups: Option>, - - /// Time cursor. - /// - /// Cursor used by subscription loop to identify point in time after - /// which updates will be delivered. + /// Custom cursor used by subscription loop to identify point in time + /// after which updates will be delivered. cursor: Option, }, @@ -112,22 +98,16 @@ pub(crate) enum SubscribeState { /// System wasn't able to perform successful initial subscription after /// fixed number of attempts. HandshakeFailed { - /// Optional list of channels. - /// - /// List of channels which has been used during recently failed initial - /// subscription. - channels: Option>, - - /// Optional list of channel groups. + /// User input with channels and groups. /// - /// List of channel groups which has been used during recently failed - /// initial subscription. - channel_groups: Option>, + /// Object contains list of channels and groups which has been used + /// during recently failed initial subscription. + input: SubscribeInput, - /// Time cursor. + /// Custom time cursor. /// - /// Cursor used by subscription loop to identify point in time after - /// which updates will be delivered. + /// Custom cursor used by subscription loop to identify point in time + /// after which updates will be delivered. cursor: Option, /// Initial subscribe attempt failure reason. @@ -141,16 +121,11 @@ pub(crate) enum SubscribeState { /// /// [`PubNub`]:https://www.pubnub.com/ Receiving { - /// Optional list of channels. - /// - /// List of channels for which real-time updates will be delivered. - channels: Option>, - - /// Optional list of channel groups. + /// User input with channels and groups. /// - /// List of channel groups for which real-time updates will be - /// delivered. - channel_groups: Option>, + /// Object contains list of channels and groups which real-time updates + /// will be delivered. + input: SubscribeInput, /// Time cursor. /// @@ -163,17 +138,11 @@ pub(crate) enum SubscribeState { /// /// The system is recovering after the updates receiving attempt failed. ReceiveReconnecting { - /// Optional list of channels. + /// User input with channels and groups. /// - /// List of channels which has been used during recently failed receive - /// updates. - channels: Option>, - - /// Optional list of channel groups. - /// - /// List of channel groups which has been used during recently failed - /// receive updates. - channel_groups: Option>, + /// Object contains list of channels and groups which has been used + /// during recently failed receive updates. + input: SubscribeInput, /// Time cursor. /// @@ -191,21 +160,12 @@ pub(crate) enum SubscribeState { }, /// Updates receiving stopped state. - /// - /// Subscription state machine state, which is set when - /// [`SubscribeEvent::Disconnect`] event sent while in - /// [`SubscribeState::Handshaking`] or - /// [`SubscribeState::HandshakeReconnecting`] state. ReceiveStopped { - /// Optional list of channels. + /// User input with channels and groups. /// - /// List of channels for which updates receive stopped. - channels: Option>, - - /// Optional list of channels. - /// - /// List of channel groups for which updates receive stopped. - channel_groups: Option>, + /// Object contains list of channels and groups for which updates + /// receive stopped. + input: SubscribeInput, /// Time cursor. /// @@ -218,17 +178,11 @@ pub(crate) enum SubscribeState { /// /// System wasn't able to receive updates after fixed number of attempts. ReceiveFailed { - /// Optional list of channels. - /// - /// List of channels which has been used during recently failed receive - /// updates. - channels: Option>, - - /// Optional list of channel groups. + /// User input with channels and groups. /// - /// List of channel groups which has been used during recently failed - /// receive updates. - channel_groups: Option>, + /// Object contains list of channels and groups which has been used + /// during recently failed receive updates. + input: SubscribeInput, /// Time cursor. /// @@ -251,8 +205,7 @@ impl SubscribeState { match self { Self::Unsubscribed => Some(self.transition_to( Self::Handshaking { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: SubscribeInput::new(channels, channel_groups), cursor: None, }, None, @@ -261,16 +214,14 @@ impl SubscribeState { | Self::HandshakeReconnecting { cursor, .. } | Self::HandshakeFailed { cursor, .. } => Some(self.transition_to( Self::Handshaking { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: SubscribeInput::new(channels, channel_groups), cursor: cursor.clone(), }, None, )), Self::HandshakeStopped { cursor, .. } => Some(self.transition_to( Self::Handshaking { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: SubscribeInput::new(channels, channel_groups), cursor: cursor.clone(), }, None, @@ -278,8 +229,7 @@ impl SubscribeState { Self::Receiving { cursor, .. } | Self::ReceiveReconnecting { cursor, .. } => { Some(self.transition_to( Self::Receiving { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: SubscribeInput::new(channels, channel_groups), cursor: cursor.clone(), }, None, @@ -287,16 +237,14 @@ impl SubscribeState { } Self::ReceiveFailed { cursor, .. } => Some(self.transition_to( Self::Handshaking { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: SubscribeInput::new(channels, channel_groups), cursor: Some(cursor.clone()), }, None, )), Self::ReceiveStopped { cursor, .. } => Some(self.transition_to( Self::ReceiveStopped { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: SubscribeInput::new(channels, channel_groups), cursor: cursor.clone(), }, None, @@ -317,8 +265,7 @@ impl SubscribeState { match self { Self::Unsubscribed => Some(self.transition_to( Self::Handshaking { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: SubscribeInput::new(channels, channel_groups), cursor: Some(restore_cursor.clone()), }, None, @@ -328,32 +275,28 @@ impl SubscribeState { | Self::HandshakeFailed { cursor, .. } | Self::HandshakeStopped { cursor, .. } => Some(self.transition_to( Self::Handshaking { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: SubscribeInput::new(channels, channel_groups), cursor: Some(cursor.clone().unwrap_or(restore_cursor.clone())), }, None, )), Self::Receiving { .. } | Self::ReceiveReconnecting { .. } => Some(self.transition_to( Self::Receiving { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: SubscribeInput::new(channels, channel_groups), cursor: restore_cursor.clone(), }, None, )), Self::ReceiveFailed { .. } => Some(self.transition_to( Self::Handshaking { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: SubscribeInput::new(channels, channel_groups), cursor: Some(restore_cursor.clone()), }, None, )), Self::ReceiveStopped { .. } => Some(self.transition_to( Self::ReceiveStopped { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: SubscribeInput::new(channels, channel_groups), cursor: restore_cursor.clone(), }, None, @@ -370,20 +313,10 @@ impl SubscribeState { next_cursor: &SubscribeCursor, ) -> Option> { match self { - Self::Handshaking { - channels, - channel_groups, - cursor, - } - | Self::HandshakeReconnecting { - channels, - channel_groups, - cursor, - .. - } => Some(self.transition_to( + Self::Handshaking { input, cursor } + | Self::HandshakeReconnecting { input, cursor, .. } => Some(self.transition_to( Self::Receiving { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: input.clone(), cursor: cursor.clone().unwrap_or(next_cursor.clone()), }, Some(vec![EmitStatus(SubscribeStatus::Connected)]), @@ -398,14 +331,9 @@ impl SubscribeState { reason: &PubNubError, ) -> Option> { match self { - Self::Handshaking { - channels, - channel_groups, - cursor, - } => Some(self.transition_to( + Self::Handshaking { input, cursor } => Some(self.transition_to( Self::HandshakeReconnecting { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: input.clone(), cursor: cursor.clone(), attempts: 1, reason: reason.clone(), @@ -426,15 +354,13 @@ impl SubscribeState { ) -> Option> { match self { Self::HandshakeReconnecting { - channels, - channel_groups, + input, cursor, attempts, .. } => Some(self.transition_to( Self::HandshakeReconnecting { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: input.clone(), cursor: cursor.clone(), attempts: attempts + 1, reason: reason.clone(), @@ -454,15 +380,9 @@ impl SubscribeState { reason: &PubNubError, ) -> Option> { match self { - Self::HandshakeReconnecting { - channels, - channel_groups, - cursor, - .. - } => Some(self.transition_to( + Self::HandshakeReconnecting { input, cursor, .. } => Some(self.transition_to( Self::HandshakeFailed { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: input.clone(), cursor: cursor.clone(), reason: reason.clone(), }, @@ -484,23 +404,15 @@ impl SubscribeState { messages: &[Update], ) -> Option> { match self { - Self::Receiving { - channels, - channel_groups, - .. + Self::Receiving { input, .. } | Self::ReceiveReconnecting { input, .. } => { + Some(self.transition_to( + Self::Receiving { + input: input.clone(), + cursor: cursor.clone(), + }, + Some(vec![EmitMessages(messages.to_vec())]), + )) } - | Self::ReceiveReconnecting { - channels, - channel_groups, - .. - } => Some(self.transition_to( - Self::Receiving { - channels: channels.clone(), - channel_groups: channel_groups.clone(), - cursor: cursor.clone(), - }, - Some(vec![EmitMessages(messages.to_vec())]), - )), _ => None, } } @@ -511,15 +423,9 @@ impl SubscribeState { reason: &PubNubError, ) -> Option> { match self { - Self::Receiving { - channels, - channel_groups, - cursor, - .. - } => Some(self.transition_to( + Self::Receiving { input, cursor, .. } => Some(self.transition_to( Self::ReceiveReconnecting { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: input.clone(), cursor: cursor.clone(), attempts: 1, reason: reason.clone(), @@ -540,15 +446,13 @@ impl SubscribeState { ) -> Option> { match self { Self::ReceiveReconnecting { - channels, - channel_groups, + input, attempts, cursor, .. } => Some(self.transition_to( Self::ReceiveReconnecting { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: input.clone(), cursor: cursor.clone(), attempts: attempts + 1, reason: reason.clone(), @@ -568,15 +472,9 @@ impl SubscribeState { reason: &PubNubError, ) -> Option> { match self { - Self::ReceiveReconnecting { - channels, - channel_groups, - cursor, - .. - } => Some(self.transition_to( + Self::ReceiveReconnecting { input, cursor, .. } => Some(self.transition_to( Self::ReceiveFailed { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: input.clone(), cursor: cursor.clone(), reason: reason.clone(), }, @@ -592,42 +490,23 @@ impl SubscribeState { /// channels / groups or temporally stop any activity. fn disconnect_transition(&self) -> Option> { match self { - Self::Handshaking { - channels, - channel_groups, - cursor, - } - | Self::HandshakeReconnecting { - channels, - channel_groups, - cursor, - .. - } => Some(self.transition_to( + Self::Handshaking { input, cursor } + | Self::HandshakeReconnecting { input, cursor, .. } => Some(self.transition_to( Self::HandshakeStopped { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: input.clone(), cursor: cursor.clone(), }, None, )), - Self::Receiving { - channels, - channel_groups, - cursor, + Self::Receiving { input, cursor } | Self::ReceiveReconnecting { input, cursor, .. } => { + Some(self.transition_to( + Self::ReceiveStopped { + input: input.clone(), + cursor: cursor.clone(), + }, + Some(vec![EmitStatus(SubscribeStatus::Disconnected)]), + )) } - | Self::ReceiveReconnecting { - channels, - channel_groups, - cursor, - .. - } => Some(self.transition_to( - Self::ReceiveStopped { - channels: channels.clone(), - channel_groups: channel_groups.clone(), - cursor: cursor.clone(), - }, - Some(vec![EmitStatus(SubscribeStatus::Disconnected)]), - )), _ => None, } } @@ -639,42 +518,23 @@ impl SubscribeState { /// after reconnection failures. fn reconnect_transition(&self) -> Option> { match self { - Self::HandshakeStopped { - channels, - channel_groups, - cursor, - } - | Self::HandshakeFailed { - channels, - channel_groups, - cursor, - .. - } => Some(self.transition_to( + Self::HandshakeStopped { input, cursor } + | Self::HandshakeFailed { input, cursor, .. } => Some(self.transition_to( Self::Handshaking { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: input.clone(), cursor: cursor.clone(), }, None, )), - Self::ReceiveStopped { - channels, - channel_groups, - cursor, + Self::ReceiveStopped { input, cursor } | Self::ReceiveFailed { input, cursor, .. } => { + Some(self.transition_to( + Self::Handshaking { + input: input.clone(), + cursor: Some(cursor.clone()), + }, + None, + )) } - | Self::ReceiveFailed { - channels, - channel_groups, - cursor, - .. - } => Some(self.transition_to( - Self::Handshaking { - channels: channels.clone(), - channel_groups: channel_groups.clone(), - cursor: Some(cursor.clone()), - }, - None, - )), _ => None, } } @@ -695,46 +555,32 @@ impl State for SubscribeState { fn enter(&self) -> Option> { match self { - SubscribeState::Handshaking { - channels, - channel_groups, - cursor, - } => Some(vec![Handshake { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + Self::Handshaking { input, cursor } => Some(vec![Handshake { + input: input.clone(), cursor: cursor.clone(), }]), Self::HandshakeReconnecting { - channels, - channel_groups, + input, cursor, attempts, reason, } => Some(vec![HandshakeReconnect { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: input.clone(), cursor: cursor.clone(), attempts: *attempts, reason: reason.clone(), }]), - Self::Receiving { - channels, - channel_groups, - cursor, - } => Some(vec![Receive { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + Self::Receiving { input, cursor } => Some(vec![Receive { + input: input.clone(), cursor: cursor.clone(), }]), Self::ReceiveReconnecting { - channels, - channel_groups, + input, cursor, attempts, reason, } => Some(vec![ReceiveReconnect { - channels: channels.clone(), - channel_groups: channel_groups.clone(), + input: input.clone(), cursor: cursor.clone(), attempts: *attempts, reason: reason.clone(), @@ -804,8 +650,8 @@ impl State for SubscribeState { .exit() .unwrap_or(vec![]) .into_iter() - .chain(invocations.unwrap_or(vec![]).into_iter()) - .chain(state.enter().unwrap_or(vec![]).into_iter()) + .chain(invocations.unwrap_or(vec![])) + .chain(state.enter().unwrap_or(vec![])) .collect(), state, } @@ -815,12 +661,12 @@ impl State for SubscribeState { #[cfg(test)] mod should { // TODO: EE process tests should be async! + use futures::FutureExt; + use test_case::test_case; use super::*; - use crate::core::RequestRetryPolicy; - use crate::providers::futures_tokio::TokioRuntime; use crate::{ - core::event_engine::EventEngine, + core::{event_engine::EventEngine, RequestRetryPolicy}, dx::subscribe::{ event_engine::{ effects::{ @@ -831,9 +677,8 @@ mod should { result::SubscribeResult, }, lib::alloc::sync::Arc, + providers::futures_tokio::RuntimeTokio, }; - use futures::FutureExt; - use test_case::test_case; fn event_engine( start_state: SubscribeState, @@ -869,7 +714,7 @@ mod should { tx, ), start_state, - TokioRuntime, + RuntimeTokio, ) } @@ -880,8 +725,10 @@ mod should { channel_groups: Some(vec!["gr1".to_string()]), }, SubscribeState::Handshaking { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, }; "to handshaking on subscription changed" @@ -894,8 +741,10 @@ mod should { cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Handshaking { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "10".into(), region: 1 }) }; "to handshaking on subscription restored" @@ -925,8 +774,10 @@ mod should { #[test_case( SubscribeState::Handshaking { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, }, SubscribeEvent::SubscriptionChanged { @@ -934,16 +785,20 @@ mod should { channel_groups: Some(vec!["gr2".to_string()]), }, SubscribeState::Handshaking { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: None, }; "to handshaking on subscription changed" )] #[test_case( SubscribeState::Handshaking { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), }, SubscribeEvent::SubscriptionChanged { @@ -951,24 +806,30 @@ mod should { channel_groups: Some(vec!["gr2".to_string()]), }, SubscribeState::Handshaking { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), }; "to handshaking with custom cursor on subscription changed" )] #[test_case( SubscribeState::Handshaking { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, }, SubscribeEvent::HandshakeFailure { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeState::HandshakeReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, @@ -977,16 +838,20 @@ mod should { )] #[test_case( SubscribeState::Handshaking { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), }, SubscribeEvent::HandshakeFailure { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeState::HandshakeReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, @@ -995,68 +860,86 @@ mod should { )] #[test_case( SubscribeState::Handshaking { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]), + ), cursor: None, }, SubscribeEvent::Disconnect, SubscribeState::HandshakeStopped { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, }; "to handshake stopped on disconnect" )] #[test_case( SubscribeState::Handshaking { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), }, SubscribeEvent::Disconnect, SubscribeState::HandshakeStopped { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), }; "to handshake stopped with custom cursor on disconnect" )] #[test_case( SubscribeState::Handshaking { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, }, SubscribeEvent::HandshakeSuccess { cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Receiving { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }; "to receiving on handshake success" )] #[test_case( SubscribeState::Handshaking { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), }, SubscribeEvent::HandshakeSuccess { cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Receiving { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "20".into(), region: 1 } }; "to receiving with custom cursor on handshake success" )] #[test_case( SubscribeState::Handshaking { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, }, SubscribeEvent::SubscriptionRestored { @@ -1065,16 +948,20 @@ mod should { cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }, SubscribeState::Handshaking { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "10".into(), region: 1 }), }; "to handshaking on subscription restored" )] #[test_case( SubscribeState::Handshaking { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), }, SubscribeEvent::SubscriptionRestored { @@ -1083,24 +970,30 @@ mod should { cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Handshaking { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), }; "to handshaking with custom cursor on subscription restored" )] #[test_case( SubscribeState::Handshaking { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, }, SubscribeEvent::HandshakeReconnectGiveUp { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, } }, SubscribeState::Handshaking { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, }; "to not change on unexpected event" @@ -1121,8 +1014,10 @@ mod should { #[test_case( SubscribeState::HandshakeReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, @@ -1131,8 +1026,10 @@ mod should { reason: PubNubError::Transport { details: "Test reason on error".to_string(), response: None, }, }, SubscribeState::HandshakeReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, attempts: 2, reason: PubNubError::Transport { details: "Test reason on error".to_string(), response: None, }, @@ -1141,8 +1038,10 @@ mod should { )] #[test_case( SubscribeState::HandshakeReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, @@ -1151,8 +1050,10 @@ mod should { reason: PubNubError::Transport { details: "Test reason on error".to_string(), response: None, }, }, SubscribeState::HandshakeReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), attempts: 2, reason: PubNubError::Transport { details: "Test reason on error".to_string(), response: None, }, @@ -1161,8 +1062,10 @@ mod should { )] #[test_case( SubscribeState::HandshakeReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, @@ -1172,16 +1075,20 @@ mod should { channel_groups: Some(vec!["gr2".to_string()]), }, SubscribeState::Handshaking { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: None, }; "to handshaking on subscription change" )] #[test_case( SubscribeState::HandshakeReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, @@ -1191,48 +1098,60 @@ mod should { channel_groups: Some(vec!["gr2".to_string()]), }, SubscribeState::Handshaking { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), }; "to handshaking with custom cursor on subscription change" )] #[test_case( SubscribeState::HandshakeReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::Disconnect, SubscribeState::HandshakeStopped { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, }; "to handshake stopped on disconnect" )] #[test_case( SubscribeState::HandshakeReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::Disconnect, SubscribeState::HandshakeStopped { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), }; "to handshake stopped with custom cursor on disconnect" )] #[test_case( SubscribeState::HandshakeReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, @@ -1241,8 +1160,10 @@ mod should { reason: PubNubError::Transport { details: "Test give up reason".to_string(), response: None, } }, SubscribeState::HandshakeFailed { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, reason: PubNubError::Transport { details: "Test give up reason".to_string(), response: None, } }; @@ -1250,8 +1171,10 @@ mod should { )] #[test_case( SubscribeState::HandshakeReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, @@ -1260,8 +1183,10 @@ mod should { reason: PubNubError::Transport { details: "Test give up reason".to_string(), response: None, } }, SubscribeState::HandshakeFailed { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), reason: PubNubError::Transport { details: "Test give up reason".to_string(), response: None, } }; @@ -1269,8 +1194,10 @@ mod should { )] #[test_case( SubscribeState::HandshakeReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, @@ -1279,16 +1206,20 @@ mod should { cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Receiving { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }; "to receiving on reconnect success" )] #[test_case( SubscribeState::HandshakeReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, @@ -1297,16 +1228,20 @@ mod should { cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Receiving { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "20".into(), region: 1 } }; "to receiving with custom cursor on reconnect success" )] #[test_case( SubscribeState::HandshakeReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, @@ -1317,16 +1252,20 @@ mod should { cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Handshaking { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "10".into(), region: 1 }) }; "to handshaking on subscription restored" )] #[test_case( SubscribeState::HandshakeReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, @@ -1337,16 +1276,20 @@ mod should { cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Handshaking { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), }; "to handshaking with custom cursor on subscription restored" )] #[test_case( SubscribeState::HandshakeReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, @@ -1356,8 +1299,10 @@ mod should { messages: vec![] }, SubscribeState::HandshakeReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, @@ -1380,8 +1325,10 @@ mod should { #[test_case( SubscribeState::HandshakeFailed { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, @@ -1390,16 +1337,20 @@ mod should { channel_groups: Some(vec!["gr2".to_string()]), }, SubscribeState::Handshaking { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: None, }; "to handshaking on subscription changed" )] #[test_case( SubscribeState::HandshakeFailed { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, @@ -1408,46 +1359,58 @@ mod should { channel_groups: Some(vec!["gr2".to_string()]), }, SubscribeState::Handshaking { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), }; "to handshaking with custom cursor on subscription changed" )] #[test_case( SubscribeState::HandshakeFailed { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::Reconnect, SubscribeState::Handshaking { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, }; "to handshaking on reconnect" )] #[test_case( SubscribeState::HandshakeFailed { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, SubscribeEvent::Reconnect, SubscribeState::Handshaking { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), }; "to handshaking with custom cursor on reconnect" )] #[test_case( SubscribeState::HandshakeFailed { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, @@ -1457,16 +1420,20 @@ mod should { cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Handshaking { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "10".into(), region: 1 }) }; "to handshaking on subscription restored" )] #[test_case( SubscribeState::HandshakeFailed { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, @@ -1476,16 +1443,20 @@ mod should { cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Handshaking { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }) }; "to handshaking with custom cursor on subscription restored" )] #[test_case( SubscribeState::HandshakeFailed { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }, @@ -1494,8 +1465,10 @@ mod should { messages: vec![] }, SubscribeState::HandshakeFailed { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, }, }; @@ -1517,36 +1490,46 @@ mod should { #[test_case( SubscribeState::HandshakeStopped { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, }, SubscribeEvent::Reconnect, SubscribeState::Handshaking { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, }; "to handshaking on reconnect" )] #[test_case( SubscribeState::HandshakeStopped { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), }, SubscribeEvent::Reconnect, SubscribeState::Handshaking { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), }; "to handshaking with custom cursor on reconnect" )] #[test_case( SubscribeState::HandshakeStopped { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, }, SubscribeEvent::SubscriptionRestored { @@ -1555,16 +1538,20 @@ mod should { cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Handshaking { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "10".into(), region: 1 }) }; "to handshaking on subscription restored" )] #[test_case( SubscribeState::HandshakeStopped { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), }, SubscribeEvent::SubscriptionRestored { @@ -1573,16 +1560,20 @@ mod should { cursor: SubscribeCursor { timetoken: "10".into(), region: 1 } }, SubscribeState::Handshaking { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "20".into(), region: 1 }), }; "to handshaking with custom cursor on subscription restored" )] #[test_case( SubscribeState::HandshakeStopped { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, }, SubscribeEvent::ReceiveSuccess { @@ -1590,8 +1581,10 @@ mod should { messages: vec![] }, SubscribeState::HandshakeStopped { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: None, }; "to not change on unexpected event" @@ -1612,8 +1605,10 @@ mod should { #[test_case( SubscribeState::Receiving { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::SubscriptionChanged { @@ -1621,16 +1616,20 @@ mod should { channel_groups: Some(vec!["gr2".to_string()]), }, SubscribeState::Receiving { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }; "to receiving on subscription changed" )] #[test_case( SubscribeState::Receiving { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::SubscriptionRestored { @@ -1639,16 +1638,20 @@ mod should { cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, }, SubscribeState::Receiving { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, }; "to receiving on subscription restored" )] #[test_case( SubscribeState::Receiving { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::ReceiveSuccess { @@ -1656,24 +1659,30 @@ mod should { messages: vec![] }, SubscribeState::Receiving { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, }; "to receiving on receive success" )] #[test_case( SubscribeState::Receiving { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::ReceiveFailure { reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, } }, SubscribeState::ReceiveReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, attempts: 1, reason: PubNubError::Transport { details: "Test reason".to_string(), response: None, } @@ -1682,30 +1691,38 @@ mod should { )] #[test_case( SubscribeState::Receiving { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::Disconnect, SubscribeState::ReceiveStopped { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }; "to receive stopped on disconnect" )] #[test_case( SubscribeState::Receiving { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::HandshakeSuccess { cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, }, SubscribeState::Receiving { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }; "to not change on unexpected event" @@ -1726,8 +1743,10 @@ mod should { #[test_case( SubscribeState::ReceiveReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, attempts: 1, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } @@ -1736,8 +1755,10 @@ mod should { reason: PubNubError::Transport { details: "Test reconnect error".to_string(), response: None, } }, SubscribeState::ReceiveReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, attempts: 2, reason: PubNubError::Transport { details: "Test reconnect error".to_string(), response: None, } @@ -1746,8 +1767,10 @@ mod should { )] #[test_case( SubscribeState::ReceiveReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, attempts: 1, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } @@ -1757,16 +1780,20 @@ mod should { channel_groups: Some(vec!["gr2".to_string()]), }, SubscribeState::Receiving { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }; "to receiving on subscription changed" )] #[test_case( SubscribeState::ReceiveReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, attempts: 1, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } @@ -1777,32 +1804,40 @@ mod should { cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, }, SubscribeState::Receiving { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, }; "to receiving on subscription restored" )] #[test_case( SubscribeState::ReceiveReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, attempts: 1, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, SubscribeEvent::Disconnect, SubscribeState::ReceiveStopped { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }; "to receive stopped on disconnect" )] #[test_case( SubscribeState::ReceiveReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, attempts: 1, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } @@ -1811,8 +1846,10 @@ mod should { reason: PubNubError::Transport { details: "Test give up error".to_string(), response: None, } }, SubscribeState::ReceiveFailed { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, reason: PubNubError::Transport { details: "Test give up error".to_string(), response: None, } }; @@ -1820,8 +1857,10 @@ mod should { )] #[test_case( SubscribeState::ReceiveReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, attempts: 1, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } @@ -1830,8 +1869,10 @@ mod should { cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, }, SubscribeState::ReceiveReconnecting { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, attempts: 1, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } @@ -1854,8 +1895,10 @@ mod should { #[test_case( SubscribeState::ReceiveFailed { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, @@ -1864,16 +1907,20 @@ mod should { channel_groups: Some(vec!["gr2".to_string()]), }, SubscribeState::Handshaking { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "10".into(), region: 1 }), }; "to handshaking on subscription changed" )] #[test_case( SubscribeState::ReceiveFailed { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, @@ -1883,31 +1930,39 @@ mod should { cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, }, SubscribeState::Handshaking { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "100".into(), region: 1 }), }; "to handshaking on subscription restored" )] #[test_case( SubscribeState::ReceiveFailed { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, SubscribeEvent::Reconnect, SubscribeState::Handshaking { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "10".into(), region: 1 }), }; "to handshaking on reconnect" )] #[test_case( SubscribeState::ReceiveFailed { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }, @@ -1915,8 +1970,10 @@ mod should { cursor: SubscribeCursor { timetoken: "100".into(), region: 1 } }, SubscribeState::ReceiveFailed { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, reason: PubNubError::Transport { details: "Test error".to_string(), response: None, } }; @@ -1938,22 +1995,28 @@ mod should { #[test_case( SubscribeState::ReceiveStopped { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::Reconnect, SubscribeState::Handshaking { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: Some(SubscribeCursor { timetoken: "10".into(), region: 1 }), }; "to handshaking on reconnect" )] #[test_case( SubscribeState::ReceiveStopped { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::SubscriptionChanged { @@ -1961,16 +2024,20 @@ mod should { channel_groups: Some(vec!["gr2".to_string()]), }, SubscribeState::ReceiveStopped { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }; "to receive stopped on subscription changed" )] #[test_case( SubscribeState::ReceiveStopped { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::SubscriptionRestored { @@ -1979,24 +2046,30 @@ mod should { cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, }, SubscribeState::ReceiveStopped { - channels: Some(vec!["ch2".to_string()]), - channel_groups: Some(vec!["gr2".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch2".to_string()]), + &Some(vec!["gr2".to_string()]) + ), cursor: SubscribeCursor { timetoken: "100".into(), region: 1 }, }; "to receive stopped on subscription restored" )] #[test_case( SubscribeState::ReceiveStopped { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }, SubscribeEvent::HandshakeSuccess { cursor: SubscribeCursor { timetoken: "100".into(), region: 1 } }, SubscribeState::ReceiveStopped { - channels: Some(vec!["ch1".to_string()]), - channel_groups: Some(vec!["gr1".to_string()]), + input: SubscribeInput::new( + &Some(vec!["ch1".to_string()]), + &Some(vec!["gr1".to_string()]) + ), cursor: SubscribeCursor { timetoken: "10".into(), region: 1 }, }; "to not change on unexpected event" diff --git a/src/dx/subscribe/event_engine/types.rs b/src/dx/subscribe/event_engine/types.rs new file mode 100644 index 00000000..cad3a70a --- /dev/null +++ b/src/dx/subscribe/event_engine/types.rs @@ -0,0 +1,729 @@ +//! Subscribe event engine module types. +//! +//! This module contains the [`SubscribeInput`] type, which represents +//! user-provided channels and groups for which real-time updates should be +//! retrieved from the [`PubNub`] network. +//! +//! [`PubNub`]:https://www.pubnub.com/ + +use crate::{ + core::PubNubError, + lib::{ + alloc::collections::HashSet, + core::ops::{Add, AddAssign, Sub, SubAssign}, + }, + subscribe::SubscribeCursor, +}; + +/// User-provided channels and groups for subscription. +/// +/// Object contains information about channels and groups for which real-time +/// updates should be retrieved from the [`PubNub`] network. +/// +/// [`PubNub`]:https://www.pubnub.com/ +#[derive(Clone, Debug, PartialEq)] +pub struct SubscribeInput { + /// Optional list of channels. + /// + /// List of channels for which real-time updates should be retrieved + /// from the [`PubNub`] network. + /// + /// List is optional if there is at least one `channel_group` provided. + /// + /// [`PubNub`]:https://www.pubnub.com/ + pub channels: Option>, + + /// Optional list of channel groups. + /// + /// List of channel groups for which real-time updates should be retrieved + /// from the [`PubNub`] network. + /// + /// [`PubNub`]:https://www.pubnub.com/ + pub channel_groups: Option>, + + /// Whether user input is empty or not. + pub is_empty: bool, +} + +impl SubscribeInput { + pub fn new(channels: &Option>, channel_groups: &Option>) -> Self { + let channels = channels.as_ref().map(|channels| { + channels.iter().fold(HashSet::new(), |mut acc, channel| { + acc.insert(channel.clone()); + acc + }) + }); + let channel_groups = channel_groups.as_ref().map(|groups| { + groups.iter().fold(HashSet::new(), |mut acc, group| { + acc.insert(group.clone()); + acc + }) + }); + + let channel_groups_is_empty = channel_groups.as_ref().map_or(true, |set| set.is_empty()); + let channels_is_empty = channels.as_ref().map_or(true, |set| set.is_empty()); + + Self { + channels, + channel_groups, + is_empty: channel_groups_is_empty && channels_is_empty, + } + } + + pub fn channels(&self) -> Option> { + self.channels.clone().map(|ch| ch.into_iter().collect()) + } + + pub fn contains_channel(&self, channel: &String) -> bool { + self.channels + .as_ref() + .map_or(false, |channels| channels.contains(channel)) + } + + pub fn channel_groups(&self) -> Option> { + self.channel_groups + .clone() + .map(|ch| ch.into_iter().collect()) + } + + pub fn contains_channel_group(&self, channel_group: &Option) -> bool { + let Some(channel_group) = channel_group else { + return false; + }; + + self.channel_groups + .as_ref() + .map_or(false, |channel_groups| { + channel_groups.contains(channel_group) + }) + } + + fn join_sets( + &self, + lhs: &Option>, + rhs: &Option>, + ) -> Option> { + match (lhs, rhs) { + (Some(lhs), Some(rhs)) => Some(lhs.iter().cloned().chain(rhs.to_owned()).collect()), + (Some(lhs), None) => Some(lhs.to_owned()), + (None, Some(rhs)) => Some(rhs.to_owned()), + _ => None, + } + } + + fn sub_sets( + &self, + lhs: &Option>, + rhs: &Option>, + ) -> Option> { + match (lhs.to_owned(), rhs.to_owned()) { + (Some(lhs), Some(rhs)) => Some(&lhs - &rhs).filter(|diff| !diff.is_empty()), + (Some(lhs), None) => Some(lhs), + _ => None, + } + } +} + +impl Add for SubscribeInput { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + let channel_groups = self.join_sets(&self.channel_groups, &rhs.channel_groups); + let channels = self.join_sets(&self.channels, &rhs.channels); + let channel_groups_is_empty = channel_groups.as_ref().map_or(true, |set| set.is_empty()); + let channels_is_empty = channels.as_ref().map_or(true, |set| set.is_empty()); + + Self { + channels, + channel_groups, + is_empty: channel_groups_is_empty && channels_is_empty, + } + } +} + +impl AddAssign for SubscribeInput { + fn add_assign(&mut self, rhs: Self) { + let channel_groups = self.join_sets(&self.channel_groups, &rhs.channel_groups); + let channels = self.join_sets(&self.channels, &rhs.channels); + let channel_groups_is_empty = channel_groups.as_ref().map_or(true, |set| set.is_empty()); + let channels_is_empty = channels.as_ref().map_or(true, |set| set.is_empty()); + + self.channels = channels; + self.channel_groups = channel_groups; + self.is_empty = channel_groups_is_empty && channels_is_empty; + } +} + +impl Sub for SubscribeInput { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + let channel_groups = self.sub_sets(&self.channel_groups, &rhs.channel_groups); + let channels = self.sub_sets(&self.channels, &rhs.channels); + let channel_groups_is_empty = channel_groups.as_ref().map_or(true, |set| set.is_empty()); + let channels_is_empty = channels.as_ref().map_or(true, |set| set.is_empty()); + + Self { + channels, + channel_groups, + is_empty: channel_groups_is_empty && channels_is_empty, + } + } +} + +impl SubAssign for SubscribeInput { + fn sub_assign(&mut self, rhs: Self) { + let channel_groups = self.sub_sets(&self.channel_groups, &rhs.channel_groups); + let channels = self.sub_sets(&self.channels, &rhs.channels); + let channel_groups_is_empty = channel_groups.as_ref().map_or(true, |set| set.is_empty()); + let channels_is_empty = channels.as_ref().map_or(true, |set| set.is_empty()); + + self.channels = channels; + self.channel_groups = channel_groups; + self.is_empty = channel_groups_is_empty && channels_is_empty; + } +} + +#[cfg(feature = "std")] +#[derive(Clone)] +/// Subscribe event engine data. +/// +/// Data objects are used by the subscribe event engine to communicate between +/// components. +pub(crate) struct SubscriptionParams<'execution> { + /// Channels from which real-time updates should be received. + pub channels: &'execution Option>, + + /// Channel groups from which real-time updates should be received. + pub channel_groups: &'execution Option>, + + /// Time cursor. + pub cursor: Option<&'execution SubscribeCursor>, + + /// How many consequent retry attempts has been made. + pub attempt: u8, + + /// Reason why previous request created by subscription event engine failed. + pub reason: Option, + + /// Effect identifier. + /// + /// Identifier of effect which requested to create request. + pub effect_id: &'execution str, +} + +#[cfg(test)] +mod it_should { + use super::*; + + #[test] + fn create_empty_input() { + let input = SubscribeInput::new(&None, &None); + assert!(input.is_empty); + } + + #[test] + fn create_input_with_unique_channels() { + let input = SubscribeInput::new( + &Some(vec![ + "channel-1".into(), + "channel-2".into(), + "channel-1".into(), + ]), + &None, + ); + + assert!(!input.is_empty); + assert_eq!(input.channels().unwrap().len(), 2); + assert_eq!( + input + .channels() + .map(|mut channels| { + channels.sort(); + channels + }) + .unwrap(), + vec!["channel-1".to_string(), "channel-2".to_string()] + ); + } + + #[test] + fn create_input_with_unique_channel_groups() { + let input = SubscribeInput::new( + &None, + &Some(vec![ + "channel-group-1".into(), + "channel-group-2".into(), + "channel-group-2".into(), + ]), + ); + + assert!(!input.is_empty); + assert_eq!(input.channel_groups().unwrap().len(), 2); + assert_eq!( + input + .channel_groups() + .map(|mut groups| { + groups.sort(); + groups + }) + .unwrap(), + vec!["channel-group-1".to_string(), "channel-group-2".to_string()] + ); + } + + #[test] + fn add_unique_channels_to_empty_input() { + let empty_input = SubscribeInput::new(&None, &None); + let input = SubscribeInput::new( + &Some(vec![ + "channel-1".into(), + "channel-2".into(), + "channel-1".into(), + ]), + &None, + ); + + assert!(!input.is_empty); + + let joint_input = empty_input + input; + + assert!(!joint_input.is_empty); + assert_eq!(joint_input.channels().unwrap().len(), 2); + assert_eq!( + joint_input + .channels() + .map(|mut channels| { + channels.sort(); + channels + }) + .unwrap(), + vec!["channel-1".to_string(), "channel-2".to_string()] + ); + assert!(joint_input.channel_groups().is_none()); + } + + #[test] + fn add_unique_channel_groups_to_empty_input() { + let empty_input = SubscribeInput::new(&None, &None); + let input = SubscribeInput::new( + &None, + &Some(vec![ + "channel-group-1".into(), + "channel-group-2".into(), + "channel-group-2".into(), + ]), + ); + + assert!(!input.is_empty); + + let joint_input = empty_input + input; + + assert!(!joint_input.is_empty); + assert!(joint_input.channels().is_none()); + assert_eq!(joint_input.channel_groups().unwrap().len(), 2); + assert_eq!( + joint_input + .channel_groups() + .map(|mut groups| { + groups.sort(); + groups + }) + .unwrap(), + vec!["channel-group-1".to_string(), "channel-group-2".to_string()] + ); + } + + #[test] + fn add_unique_channels_and_channel_groups_to_existing_input() { + let existing_input = SubscribeInput::new( + &Some(vec![ + "channel-1".into(), + "channel-4".into(), + "channel-2".into(), + ]), + &Some(vec![ + "channel-group-1".into(), + "channel-group-3".into(), + "channel-group-5".into(), + ]), + ); + let input = SubscribeInput::new( + &Some(vec![ + "channel-1".into(), + "channel-2".into(), + "channel-1".into(), + ]), + &Some(vec![ + "channel-group-1".into(), + "channel-group-2".into(), + "channel-group-2".into(), + ]), + ); + + assert!(!existing_input.is_empty); + assert!(!input.is_empty); + + let joint_input = existing_input + input; + + assert!(!joint_input.is_empty); + assert_eq!(joint_input.channels().unwrap().len(), 3); + assert_eq!(joint_input.channel_groups().unwrap().len(), 4); + assert_eq!( + joint_input + .channels() + .map(|mut channels| { + channels.sort(); + channels + }) + .unwrap(), + vec![ + "channel-1".to_string(), + "channel-2".to_string(), + "channel-4".to_string() + ] + ); + assert_eq!( + joint_input + .channel_groups() + .map(|mut groups| { + groups.sort(); + groups + }) + .unwrap(), + vec![ + "channel-group-1".to_string(), + "channel-group-2".to_string(), + "channel-group-3".to_string(), + "channel-group-5".to_string() + ] + ); + } + + #[test] + fn add_assign_unique_channels_and_channel_groups_to_existing_input() { + let mut existing_input = SubscribeInput::new( + &Some(vec![ + "channel-1".into(), + "channel-4".into(), + "channel-2".into(), + ]), + &Some(vec![ + "channel-group-1".into(), + "channel-group-3".into(), + "channel-group-5".into(), + ]), + ); + let input = SubscribeInput::new( + &Some(vec![ + "channel-1".into(), + "channel-2".into(), + "channel-1".into(), + ]), + &Some(vec![ + "channel-group-1".into(), + "channel-group-2".into(), + "channel-group-2".into(), + ]), + ); + + assert!(!existing_input.is_empty); + assert!(!input.is_empty); + + existing_input += input; + + assert!(!existing_input.is_empty); + assert_eq!(existing_input.channels().unwrap().len(), 3); + assert_eq!(existing_input.channel_groups().unwrap().len(), 4); + assert_eq!( + existing_input + .channels() + .map(|mut channels| { + channels.sort(); + channels + }) + .unwrap(), + vec![ + "channel-1".to_string(), + "channel-2".to_string(), + "channel-4".to_string() + ] + ); + assert_eq!( + existing_input + .channel_groups() + .map(|mut groups| { + groups.sort(); + groups + }) + .unwrap(), + vec![ + "channel-group-1".to_string(), + "channel-group-2".to_string(), + "channel-group-3".to_string(), + "channel-group-5".to_string() + ] + ); + } + + #[test] + fn remove_channels_from_empty_input() { + let empty_input = SubscribeInput::new(&None, &None); + let input = SubscribeInput::new( + &Some(vec![ + "channel-1".into(), + "channel-2".into(), + "channel-1".into(), + ]), + &None, + ); + + assert!(!input.is_empty); + + let diff_input = empty_input - input; + + assert!(diff_input.is_empty); + assert!(diff_input.channels().is_none()); + assert!(diff_input.channel_groups().is_none()); + } + + #[test] + fn remove_channel_groups_from_empty_input() { + let empty_input = SubscribeInput::new(&None, &None); + let input = SubscribeInput::new( + &None, + &Some(vec![ + "channel-group-1".into(), + "channel-group-2".into(), + "channel-group-1".into(), + ]), + ); + + assert!(!input.is_empty); + + let diff_input = empty_input - input; + + assert!(diff_input.is_empty); + assert!(diff_input.channels().is_none()); + assert!(diff_input.channel_groups().is_none()); + } + + #[test] + fn remove_unique_channels_from_existing_input() { + let existing_input = SubscribeInput::new( + &Some(vec![ + "channel-1".into(), + "channel-2".into(), + "channel-3".into(), + ]), + &Some(vec![ + "channel-group-1".into(), + "channel-group-2".into(), + "channel-group-3".into(), + ]), + ); + let input = SubscribeInput::new(&Some(vec!["channel-2".into(), "channel-2".into()]), &None); + + assert!(!existing_input.is_empty); + assert!(!input.is_empty); + + let diff_input = existing_input - input; + + assert!(!diff_input.is_empty); + assert_eq!(diff_input.channels().unwrap().len(), 2); + assert_eq!(diff_input.channel_groups().unwrap().len(), 3); + assert_eq!( + diff_input + .channels() + .map(|mut channels| { + channels.sort(); + channels + }) + .unwrap(), + vec!["channel-1".to_string(), "channel-3".to_string()] + ); + assert_eq!( + diff_input + .channel_groups() + .map(|mut groups| { + groups.sort(); + groups + }) + .unwrap(), + vec![ + "channel-group-1".to_string(), + "channel-group-2".to_string(), + "channel-group-3".to_string(), + ] + ); + } + + #[test] + fn remove_unique_channel_groups_from_existing_input() { + let existing_input = SubscribeInput::new( + &Some(vec![ + "channel-1".into(), + "channel-2".into(), + "channel-3".into(), + ]), + &Some(vec![ + "channel-group-1".into(), + "channel-group-2".into(), + "channel-group-3".into(), + ]), + ); + let input = SubscribeInput::new(&None, &Some(vec!["channel-group-1".into()])); + + assert!(!existing_input.is_empty); + assert!(!input.is_empty); + + let diff_input = existing_input - input; + + assert!(!diff_input.is_empty); + assert_eq!(diff_input.channels().unwrap().len(), 3); + assert_eq!(diff_input.channel_groups().unwrap().len(), 2); + assert_eq!( + diff_input + .channels() + .map(|mut channels| { + channels.sort(); + channels + }) + .unwrap(), + vec![ + "channel-1".to_string(), + "channel-2".to_string(), + "channel-3".to_string() + ] + ); + assert_eq!( + diff_input + .channel_groups() + .map(|mut groups| { + groups.sort(); + groups + }) + .unwrap(), + vec!["channel-group-2".to_string(), "channel-group-3".to_string(),] + ); + } + + #[test] + fn remove_unique_channels_and_channel_groups_from_existing_input() { + let existing_input = SubscribeInput::new( + &Some(vec![ + "channel-1".into(), + "channel-2".into(), + "channel-3".into(), + ]), + &Some(vec![ + "channel-group-1".into(), + "channel-group-2".into(), + "channel-group-3".into(), + ]), + ); + let input = SubscribeInput::new( + &Some(vec!["channel-3".into()]), + &Some(vec!["channel-group-2".into(), "channel-group-3".into()]), + ); + + assert!(!existing_input.is_empty); + assert!(!input.is_empty); + + let diff_input = existing_input - input; + + assert!(!diff_input.is_empty); + assert_eq!(diff_input.channels().unwrap().len(), 2); + assert_eq!(diff_input.channel_groups().unwrap().len(), 1); + assert_eq!( + diff_input + .channels() + .map(|mut channels| { + channels.sort(); + channels + }) + .unwrap(), + vec!["channel-1".to_string(), "channel-2".to_string(),] + ); + assert_eq!( + diff_input + .channel_groups() + .map(|mut groups| { + groups.sort(); + groups + }) + .unwrap(), + vec!["channel-group-1".to_string(),] + ); + } + + #[test] + fn remove_assign_unique_channels_and_channel_groups_from_existing_input() { + let mut existing_input = SubscribeInput::new( + &Some(vec![ + "channel-1".into(), + "channel-2".into(), + "channel-3".into(), + ]), + &Some(vec![ + "channel-group-1".into(), + "channel-group-2".into(), + "channel-group-3".into(), + ]), + ); + let input = SubscribeInput::new( + &Some(vec!["channel-3".into()]), + &Some(vec!["channel-group-2".into(), "channel-group-3".into()]), + ); + + assert!(!existing_input.is_empty); + assert!(!input.is_empty); + + existing_input -= input; + + assert!(!existing_input.is_empty); + assert_eq!(existing_input.channels().unwrap().len(), 2); + assert_eq!(existing_input.channel_groups().unwrap().len(), 1); + assert_eq!( + existing_input + .channels() + .map(|mut channels| { + channels.sort(); + channels + }) + .unwrap(), + vec!["channel-1".to_string(), "channel-2".to_string(),] + ); + assert_eq!( + existing_input + .channel_groups() + .map(|mut groups| { + groups.sort(); + groups + }) + .unwrap(), + vec!["channel-group-1".to_string(),] + ); + } + + #[test] + fn remove_all_channels_and_channel_groups_from_existing_input() { + let existing_input = SubscribeInput::new( + &Some(vec!["channel-1".into(), "channel-2".into()]), + &Some(vec!["channel-group-1".into(), "channel-group-2".into()]), + ); + let input = SubscribeInput::new( + &Some(vec!["channel-1".into(), "channel-2".into()]), + &Some(vec!["channel-group-1".into(), "channel-group-2".into()]), + ); + + assert!(!existing_input.is_empty); + assert!(!input.is_empty); + + let diff_input = existing_input - input; + + assert!(diff_input.is_empty); + assert!(diff_input.channels().is_none()); + assert!(diff_input.channel_groups().is_none()); + } +} diff --git a/src/dx/subscribe/mod.rs b/src/dx/subscribe/mod.rs index 52aef215..64120c40 100644 --- a/src/dx/subscribe/mod.rs +++ b/src/dx/subscribe/mod.rs @@ -2,23 +2,15 @@ //! //! Allows subscribe to real-time updates from channels and groups. -#[cfg(feature = "std")] -use event_engine::{SubscribeEffectHandler, SubscribeState}; -#[cfg(feature = "std")] -pub(crate) mod event_engine; - #[cfg(feature = "std")] use futures::{ future::{ready, BoxFuture}, FutureExt, }; +#[cfg(feature = "std")] +use spin::RwLock; -#[cfg(feature = "serde")] -use crate::providers::deserialization_serde::SerdeDeserializer; - -#[doc(inline)] -pub use result::{SubscribeResponseBody, Update}; -pub mod result; +use crate::dx::{pubnub_client::PubNubClientInstance, subscribe::raw::RawSubscriptionBuilder}; #[doc(inline)] pub use types::{ @@ -27,63 +19,45 @@ pub use types::{ }; pub mod types; -use crate::dx::pubnub_client::PubNubClientInstance; - #[cfg(feature = "std")] use crate::{ - core::{PubNubError, Transport}, - lib::alloc::{borrow::ToOwned, boxed::Box, string::String, vec::Vec}, + core::{Deserializer, PubNubError, Transport}, + lib::alloc::{boxed::Box, string::String, sync::Arc, vec::Vec}, subscribe::result::SubscribeResult, }; -#[cfg(any(feature = "std", feature = "serde"))] -use crate::lib::alloc::sync::Arc; - -#[cfg(feature = "std")] -use crate::core::{event_engine::EventEngine, runtime::Runtime}; - -#[cfg(feature = "std")] -pub(crate) use subscription_manager::SubscriptionManager; -#[cfg(feature = "std")] -pub(crate) mod subscription_manager; - #[cfg(feature = "std")] -pub(crate) use subscription_configuration::{ - SubscriptionConfiguration, SubscriptionConfigurationRef, +use crate::core::{ + event_engine::{CancellationTask, EventEngine}, + runtime::Runtime, }; -#[cfg(feature = "std")] -pub(crate) mod subscription_configuration; #[doc(inline)] pub use builders::*; pub mod builders; -#[cfg(feature = "std")] #[doc(inline)] -use cancel::CancellationTask; -#[cfg(feature = "std")] -mod cancel; +pub use result::{SubscribeResponseBody, Update}; +pub mod result; -#[cfg(feature = "serde")] -use self::raw::RawSubscriptionBuilder; -#[cfg(not(feature = "serde"))] -use self::raw::RawSubscriptionWithDeserializerBuilder; +#[cfg(feature = "std")] +pub(crate) use subscription_manager::SubscriptionManager; +#[cfg(feature = "std")] +pub(crate) mod subscription_manager; +#[cfg(feature = "std")] +#[doc(inline)] +use event_engine::{ + types::SubscriptionParams, SubscribeEffectHandler, SubscribeEventEngine, SubscribeState, +}; #[cfg(feature = "std")] -#[derive(Clone)] -pub(crate) struct SubscriptionParams<'execution> { - channels: &'execution Option>, - channel_groups: &'execution Option>, - cursor: Option<&'execution SubscribeCursor>, - attempt: u8, - reason: Option, - effect_id: &'execution str, -} +pub(crate) mod event_engine; #[cfg(feature = "std")] -impl PubNubClientInstance +impl PubNubClientInstance where T: Transport + Send + 'static, + D: Deserializer + 'static, { /// Create subscription listener. /// @@ -120,7 +94,6 @@ where /// .await; /// # Ok(()) /// # } - /// /// ``` /// /// For more examples see our [examples directory](https://github.com/pubnub/rust/tree/master/examples). @@ -129,135 +102,7 @@ where /// [`PubNubClient`]: crate::PubNubClient #[cfg(all(feature = "tokio", feature = "serde"))] pub fn subscribe(&self) -> SubscriptionBuilder { - use crate::providers::futures_tokio::TokioRuntime; - - self.subscribe_with_runtime(TokioRuntime) - } - - /// Create subscription listener. - /// - /// Listeners configure [`PubNubClient`] to receive real-time updates for - /// specified list of channels and groups. - /// - /// ```no_run // Starts listening for real-time updates - /// use futures::StreamExt; - /// use pubnub::dx::subscribe::{SubscribeStreamEvent, Update}; - /// - /// # #[tokio::main] - /// # async fn main() -> Result<(), Box> { - /// # use pubnub::{Keyset, PubNubClientBuilder}; - /// # - /// # let client = PubNubClientBuilder::with_reqwest_transport() - /// # .with_keyset(Keyset { - /// # subscribe_key: "demo", - /// # publish_key: Some("demo"), - /// # secret_key: None, - /// # }) - /// # .with_user_id("user_id") - /// # .build()?; - /// client - /// .subscribe() - /// .channels(["hello".into(), "world".into()].to_vec()) - /// .execute()? - /// .stream() - /// .for_each(|event| async move { - /// match event { - /// SubscribeStreamEvent::Update(update) => println!("update: {:?}", update), - /// SubscribeStreamEvent::Status(status) => println!("status: {:?}", status), - /// } - /// }) - /// .await; - /// # Ok(()) - /// # } - /// - /// ``` - /// - /// Instance of [`SubscriptionWithDeserializerBuilder`] returned. - /// [`PubNubClient`]: crate::PubNubClient - #[cfg(all(feature = "tokio", not(feature = "serde")))] - pub fn subscribe(&self) -> SubscriptionWithDeserializerBuilder { - use crate::providers::futures_tokio::TokioRuntime; - - self.subscribe_with_runtime(TokioRuntime) - } - - /// Create subscription listener. - /// - /// Listeners configure [`PubNubClient`] to receive real-time updates for - /// specified list of channels and groups. - /// - /// It takes custom runtime which will be used for detached tasks spawning - /// and delayed task execution. - /// - /// ```no_run // Starts listening for real-time updates - /// use futures::StreamExt; - /// use pubnub::dx::subscribe::{SubscribeStreamEvent, Update}; - /// use pubnub::core::runtime::Runtime; - /// use std::future::Future; - /// - /// #[derive(Clone)] - /// struct MyRuntime; - /// - /// #[async_trait::async_trait] - /// impl Runtime for MyRuntime { - /// fn spawn(&self, future: impl Future + Send + 'static) { - /// // spawn the Future - /// // e.g. tokio::spawn(future); - /// } - /// - /// async fn sleep(self, _delay: u64) { - /// // e.g. tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await - /// } - /// } - /// # #[tokio::main] - /// # async fn main() -> Result<(), Box> { - /// # use pubnub::{Keyset, PubNubClientBuilder}; - /// # - /// # let client = PubNubClientBuilder::with_reqwest_transport() - /// # .with_keyset(Keyset { - /// # subscribe_key: "demo", - /// # publish_key: Some("demo"), - /// # secret_key: None, - /// # }) - /// # .with_user_id("user_id") - /// # .build()?; - /// - /// client - /// .subscribe_with_runtime(MyRuntime) - /// .channels(["hello".into(), "world".into()].to_vec()) - /// .execute()? - /// .stream() - /// .for_each(|event| async move { - /// match event { - /// SubscribeStreamEvent::Update(update) => println!("update: {:?}", update), - /// SubscribeStreamEvent::Status(status) => println!("status: {:?}", status), - /// } - /// }) - /// .await; - /// # Ok(()) - /// # } - /// - /// ``` - /// - /// Instance of [`SubscriptionBuilder`] returned. - /// [`PubNubClient`]: crate::PubNubClient - #[cfg(feature = "serde")] - pub fn subscribe_with_runtime(&self, runtime: R) -> SubscriptionBuilder - where - R: Runtime + Send + Sync + 'static, - { - { - // Initialize subscription module when it will be first required. - let mut subscription_slot = self.subscription.write(); - if subscription_slot.is_none() { - *subscription_slot = Some(SubscriptionConfiguration { - inner: Arc::new(SubscriptionConfigurationRef { - subscription_manager: self.clone().subscription_manager(runtime), - deserializer: Some(Arc::new(SerdeDeserializer)), - }), - }); - } - } + self.configure_subscribe(); SubscriptionBuilder { subscription: Some(self.subscription.clone()), @@ -265,98 +110,33 @@ where } } - /// Create subscription listener. - /// - /// Listeners configure [`PubNubClient`] to receive real-time updates for - /// specified list of channels and groups. - /// - /// It takes custom runtime which will be used for detached tasks spawning - /// and delayed task execution. - /// ```no_run // Starts listening for real-time updates - /// use futures::StreamExt; - /// use pubnub::dx::subscribe::{SubscribeStreamEvent, Update}; - /// use pubnub::core::runtime::Runtime; - /// use std::future::Future; - /// - /// #[derive(Clone)] - /// struct MyRuntime; - /// - /// impl Runtime for MyRuntime { - /// fn spawn(&self, future: impl Future + Send + 'static) { - /// // spawn the Future - /// // e.g. tokio::spawn(future); - /// } - /// } - /// # #[tokio::main] - /// # async fn main() -> Result<(), Box> { - /// # use pubnub::{Keyset, PubNubClientBuilder}; - /// # - /// # let client = PubNubClientBuilder::with_reqwest_transport() - /// # .with_keyset(Keyset { - /// # subscribe_key: "demo", - /// # publish_key: Some("demo"), - /// # secret_key: None, - /// # }) - /// # .with_user_id("user_id") - /// # .build()?; - /// - /// client - /// .subscribe_with_runtime(MyRuntime) - /// .channels(["hello".into(), "world".into()].to_vec()) - /// .execute()? - /// .stream() - /// .for_each(|event| async move { - /// match event { - /// SubscribeStreamEvent::Update(update) => println!("update: {:?}", update), - /// SubscribeStreamEvent::Status(status) => println!("status: {:?}", status), - /// } - /// }) - /// .await; - /// # Ok(()) - /// # } - /// - /// ``` - /// - /// Instance of [`SubscriptionWithDeserializerBuilder`] returned. - /// [`PubNubClient`]: crate::PubNubClient - #[cfg(not(feature = "serde"))] - pub fn subscribe_with_runtime(&self, runtime: R) -> SubscriptionWithDeserializerBuilder - where - R: Runtime + Send + Sync + 'static, - { + pub(crate) fn configure_subscribe(&self) -> Arc>> { { // Initialize subscription module when it will be first required. - let mut subscription_slot = self.subscription.write(); - if subscription_slot.is_none() { - *subscription_slot = Some(SubscriptionConfiguration { - inner: Arc::new(SubscriptionConfigurationRef { - subscription_manager: self.clone().subscription_manager(runtime), - deserializer: None, - }), - }); + let mut slot = self.subscription.write(); + if slot.is_none() { + *slot = Some(SubscriptionManager::new(self.subscribe_event_engine())); + // *subscription_slot = + // Some(self.clone().subscription_manager(runtime)); } } - SubscriptionWithDeserializerBuilder { - subscription: self.subscription.clone(), - } + self.subscription.clone() } - pub(crate) fn subscription_manager(&mut self, runtime: R) -> SubscriptionManager - where - R: Runtime + Send + Sync + 'static, - { + pub(crate) fn subscribe_event_engine(&self) -> Arc { let channel_bound = 10; // TODO: Think about this value let emit_messages_client = self.clone(); let emit_status_client = self.clone(); let subscribe_client = self.clone(); let request_retry_delay_policy = self.config.retry_policy.clone(); let request_retry_policy = self.config.retry_policy.clone(); + let runtime = self.runtime.clone(); let runtime_sleep = runtime.clone(); let (cancel_tx, cancel_rx) = async_channel::bounded::(channel_bound); - let engine = EventEngine::new( + EventEngine::new( SubscribeEffectHandler::new( Arc::new(move |params| { let delay_in_secs = request_retry_delay_policy @@ -367,9 +147,8 @@ where subscribe_client.clone(), params.clone(), Arc::new(move || { - if let Some(de) = delay_in_secs { - // let rt = inner_runtime_sleep.clone(); - inner_runtime_sleep.clone().sleep(de).boxed() + if let Some(delay) = delay_in_secs { + inner_runtime_sleep.clone().sleep(delay).boxed() } else { ready(()).boxed() } @@ -386,9 +165,7 @@ where ), SubscribeState::Unsubscribed, runtime, - ); - - SubscriptionManager::new(engine) + ) } pub(crate) fn subscribe_call( @@ -411,29 +188,16 @@ where if let Some(channel_groups) = params.channel_groups.clone() { request = request.channel_groups(channel_groups); } - - let deserializer = { - let subscription = client - .subscription - .read() - .clone() - .expect("Subscription configuration is missing"); - subscription - .deserializer - .clone() - .expect("Deserializer is missing") - }; - let cancel_task = CancellationTask::new(cancel_rx, params.effect_id.to_owned()); // TODO: needs to be owned? request - .execute_with_cancel_and_delay(deserializer, delay, cancel_task) + .execute_with_cancel_and_delay(delay, cancel_task) .boxed() } fn emit_status(client: Self, status: &SubscribeStatus) { if let Some(manager) = client.subscription.read().as_ref() { - manager.subscription_manager.notify_new_status(status) + manager.notify_new_status(status) } } @@ -448,12 +212,12 @@ where }; if let Some(manager) = client.subscription.read().as_ref() { - manager.subscription_manager.notify_new_messages(messages) + manager.notify_new_messages(messages) } } } -impl PubNubClientInstance { +impl PubNubClientInstance { /// Create subscription listener. /// /// Listeners configure [`PubNubClient`] to receive real-time updates for @@ -486,74 +250,25 @@ impl PubNubClientInstance { /// .await; /// # Ok(()) /// # } - /// /// ``` /// /// For more examples see our [examples directory](https://github.com/pubnub/rust/tree/master/examples). /// /// Instance of [`SubscriptionBuilder`] returned. /// [`PubNubClient`]: crate::PubNubClient - #[cfg(feature = "serde")] - pub fn subscribe_raw(&self) -> RawSubscriptionBuilder { + pub fn subscribe_raw(&self) -> RawSubscriptionBuilder { RawSubscriptionBuilder { pubnub_client: Some(self.clone()), - deserializer: Some(Arc::new(SerdeDeserializer)), ..Default::default() } } - /// Create subscription listener. - /// - /// Listeners configure [`PubNubClient`] to receive real-time updates for - /// specified list of channels and groups. - /// - /// ```no_run // Starts listening for real-time updates - /// use futures::StreamExt; - /// use pubnub::dx::subscribe::{SubscribeStreamEvent, Update}; - /// - /// # #[tokio::main] - /// # async fn main() -> Result<(), Box> { - /// # use pubnub::{Keyset, PubNubClientBuilder}; - /// # - /// # let client = PubNubClientBuilder::with_reqwest_transport() - /// # .with_keyset(Keyset { - /// # subscribe_key: "demo", - /// # publish_key: Some("demo"), - /// # secret_key: None, - /// # }) - /// # .with_user_id("user_id") - /// # .build()?; - /// client - /// .subscribe_raw() - /// .channels(["hello".into(), "world".into()].to_vec()) - /// .execute()? - /// .stream() - /// .for_each(|update| async move { - /// println!("Received update: {:?}", update); - /// }) - /// .await; - /// # Ok(()) - /// # } - /// - /// ``` - /// - /// For more examples see our [examples directory](https://github.com/pubnub/rust/tree/master/examples). - /// - /// Instance of [`SubscriptionBuilder`] returned. - /// [`PubNubClient`]: crate::PubNubClient - #[cfg(not(feature = "serde"))] - pub fn subscribe_raw(&self) -> RawSubscriptionWithDeserializerBuilder { - RawSubscriptionWithDeserializerBuilder { - client: self.clone(), - } - } - /// Create subscribe request builder. /// This method is used to create events stream for real-time updates on /// passed list of channels and groups. /// /// Instance of [`SubscribeRequestBuilder`] returned. - pub(crate) fn subscribe_request(&self) -> SubscribeRequestBuilder { + pub(crate) fn subscribe_request(&self) -> SubscribeRequestBuilder { SubscribeRequestBuilder { pubnub_client: Some(self.clone()), ..Default::default() @@ -565,16 +280,16 @@ impl PubNubClientInstance { #[cfg(test)] mod should { use super::*; - use crate::core::blocking; use crate::{ - core::{PubNubError, TransportRequest, TransportResponse}, + core::{blocking, PubNubError, TransportRequest, TransportResponse}, + providers::deserialization_serde::DeserializerSerde, Keyset, PubNubClientBuilder, PubNubGenericClient, }; struct MockTransport; #[async_trait::async_trait] - impl crate::core::Transport for MockTransport { + impl Transport for MockTransport { async fn send(&self, _request: TransportRequest) -> Result { Ok(TransportResponse { status: 200, @@ -621,7 +336,7 @@ mod should { ) } - fn client() -> PubNubGenericClient { + fn client() -> PubNubGenericClient { PubNubClientBuilder::with_transport(MockTransport) .with_keyset(Keyset { subscribe_key: "demo", diff --git a/src/dx/subscribe/result.rs b/src/dx/subscribe/result.rs index 95dc3f6f..8fdc3292 100644 --- a/src/dx/subscribe/result.rs +++ b/src/dx/subscribe/result.rs @@ -5,7 +5,7 @@ //! operation. use crate::{ - core::{APIErrorBody, PubNubError, ScalarValue}, + core::{service_response::APIErrorBody, PubNubError, ScalarValue}, dx::subscribe::{ types::Message, File, MessageAction, Object, Presence, {SubscribeCursor, SubscribeMessageType}, @@ -59,7 +59,8 @@ pub enum Update { /// * `leave` – some user left channel /// * `timeout` – service didn't notice user for a while /// * `interval` – bulk update on `joined`, `left` and `timeout` users. - /// * `state-change` - some user changed state associated with him on channel. + /// * `state-change` - some user changed state associated with him on + /// channel. Presence(Presence), /// Object real-time update. @@ -285,7 +286,8 @@ pub enum EnvelopePayload { /// * `leave` – some user left channel /// * `timeout` – service didn't notice user for a while /// * `interval` – bulk update on `joined`, `left` and `timeout` users. - /// * `state-change` - some user changed state associated with him on channel. + /// * `state-change` - some user changed state associated with him on + /// channel. Presence { /// Presence event type. action: Option, diff --git a/src/dx/subscribe/subscription_configuration.rs b/src/dx/subscribe/subscription_configuration.rs deleted file mode 100644 index fffc83a6..00000000 --- a/src/dx/subscribe/subscription_configuration.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! Subscriptions module configuration. -//! -//! This module contains [`SubscriptionConfiguration`] which allow user to -//! configure subscription module components. - -use crate::{ - core::Deserializer, - dx::subscribe::{result::SubscribeResponseBody, SubscriptionManager}, - lib::{ - alloc::sync::Arc, - core::{ - fmt::{Debug, Formatter, Result}, - ops::{Deref, DerefMut}, - }, - }, -}; - -/// Subscription module configuration. -#[derive(Debug)] -pub(crate) struct SubscriptionConfiguration { - pub(crate) inner: Arc, -} - -impl Deref for SubscriptionConfiguration { - type Target = SubscriptionConfigurationRef; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl DerefMut for SubscriptionConfiguration { - fn deref_mut(&mut self) -> &mut Self::Target { - Arc::get_mut(&mut self.inner).expect("Subscription stream is not unique") - } -} - -impl Clone for SubscriptionConfiguration { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - } - } -} - -/// Subscription module configuration. -pub(crate) struct SubscriptionConfigurationRef { - /// Subscription manager - pub subscription_manager: SubscriptionManager, - - /// Received data deserializer. - pub deserializer: Option>>, -} - -impl Debug for SubscriptionConfigurationRef { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - write!( - f, - "SubscriptionConfiguration {{ manager: {:?} }}", - self.subscription_manager - ) - } -} diff --git a/src/dx/subscribe/subscription_manager.rs b/src/dx/subscribe/subscription_manager.rs index 31b87235..6a5b2dcf 100644 --- a/src/dx/subscribe/subscription_manager.rs +++ b/src/dx/subscribe/subscription_manager.rs @@ -3,14 +3,17 @@ //! This module contains manager which is responsible for tracking and updating //! active subscription streams. -use super::event_engine::SubscribeEvent; -use crate::subscribe::SubscribeCursor; use crate::{ dx::subscribe::{ - event_engine::SubscribeEventEngine, result::Update, subscription::Subscription, - SubscribeStatus, + event_engine::{event::SubscribeEvent, SubscribeEventEngine, SubscribeInput}, + result::Update, + subscription::Subscription, + SubscribeCursor, SubscribeStatus, + }, + lib::{ + alloc::{sync::Arc, vec::Vec}, + core::ops::{Deref, DerefMut}, }, - lib::alloc::{sync::Arc, vec::Vec}, }; /// Active subscriptions manager. @@ -24,10 +27,58 @@ use crate::{ #[derive(Debug)] #[allow(dead_code)] pub(crate) struct SubscriptionManager { + pub(crate) inner: Arc, +} + +#[allow(dead_code)] +impl SubscriptionManager { + pub fn new(event_engine: Arc) -> Self { + Self { + inner: Arc::new(SubscriptionManagerRef { + event_engine, + subscribers: Default::default(), + }), + } + } +} + +impl Deref for SubscriptionManager { + type Target = SubscriptionManagerRef; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for SubscriptionManager { + fn deref_mut(&mut self) -> &mut Self::Target { + Arc::get_mut(&mut self.inner).expect("Presence configuration is not unique.") + } +} + +impl Clone for SubscriptionManager { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +/// Active subscriptions manager. +/// +/// [`PubNubClient`] allows to have multiple [`subscription`] objects which will +/// be used to deliver real-time updates on channels and groups specified during +/// [`subscribe`] method call. +/// +/// [`subscription`]: crate::Subscription +/// [`PubNubClient`]: crate::PubNubClient +#[derive(Debug)] +#[allow(dead_code)] +pub(crate) struct SubscriptionManagerRef { /// Subscription event engine. /// /// State machine which is responsible for subscription loop maintenance. - subscribe_event_engine: Arc, + event_engine: Arc, /// List of registered subscribers. /// @@ -35,15 +86,7 @@ pub(crate) struct SubscriptionManager { subscribers: Vec, } -#[allow(dead_code)] -impl SubscriptionManager { - pub fn new(subscribe_event_engine: Arc) -> Self { - Self { - subscribe_event_engine, - subscribers: Default::default(), - } - } - +impl SubscriptionManagerRef { pub fn notify_new_status(&self, status: &SubscribeStatus) { self.subscribers.iter().for_each(|subscription| { subscription.handle_status(status.clone()); @@ -80,46 +123,34 @@ impl SubscriptionManager { } fn change_subscription(&self) { - let channels = self - .subscribers - .iter() - .flat_map(|val| val.channels.iter()) - .cloned() - .collect::>(); - - let channel_groups = self - .subscribers - .iter() - .flat_map(|val| val.channel_groups.iter()) - .cloned() - .collect::>(); + let inputs = self.subscribers.iter().fold( + SubscribeInput::new(&None, &None), + |mut input, subscription| { + input += subscription.input.clone(); + input + }, + ); - self.subscribe_event_engine + self.event_engine .process(&SubscribeEvent::SubscriptionChanged { - channels: (!channels.is_empty()).then_some(channels), - channel_groups: (!channel_groups.is_empty()).then_some(channel_groups), + channels: inputs.channels(), + channel_groups: inputs.channel_groups(), }); } fn restore_subscription(&self, cursor: u64) { - let channels = self - .subscribers - .iter() - .flat_map(|val| val.channels.iter()) - .cloned() - .collect::>(); - - let channel_groups = self - .subscribers - .iter() - .flat_map(|val| val.channel_groups.iter()) - .cloned() - .collect::>(); + let inputs = self.subscribers.iter().fold( + SubscribeInput::new(&None, &None), + |mut input, subscription| { + input += subscription.input.clone(); + input + }, + ); - self.subscribe_event_engine + self.event_engine .process(&SubscribeEvent::SubscriptionRestored { - channels: (!channels.is_empty()).then_some(channels), - channel_groups: (!channel_groups.is_empty()).then_some(channel_groups), + channels: inputs.channels(), + channel_groups: inputs.channel_groups(), cursor: SubscribeCursor { timetoken: cursor.to_string(), region: 0, @@ -138,10 +169,9 @@ mod should { result::SubscribeResult, subscription::SubscriptionBuilder, types::Message, - SubscriptionConfiguration, SubscriptionConfigurationRef, }, lib::alloc::sync::Arc, - providers::futures_tokio::TokioRuntime, + providers::futures_tokio::RuntimeTokio, }; use spin::RwLock; @@ -169,7 +199,7 @@ mod should { cancel_tx, ), SubscribeState::Unsubscribed, - TokioRuntime, + RuntimeTokio, ) } @@ -179,12 +209,7 @@ mod should { let dummy_manager = SubscriptionManager::new(event_engine()); let subscription = SubscriptionBuilder { - subscription: Some(Arc::new(RwLock::new(Some(SubscriptionConfiguration { - inner: Arc::new(SubscriptionConfigurationRef { - subscription_manager: dummy_manager, - deserializer: None, - }), - })))), + subscription: Some(Arc::new(RwLock::new(Some(dummy_manager)))), ..Default::default() } .channels(["test".into()]) @@ -202,12 +227,7 @@ mod should { let dummy_manager = SubscriptionManager::new(event_engine()); let subscription = SubscriptionBuilder { - subscription: Some(Arc::new(RwLock::new(Some(SubscriptionConfiguration { - inner: Arc::new(SubscriptionConfigurationRef { - subscription_manager: dummy_manager, - deserializer: None, - }), - })))), + subscription: Some(Arc::new(RwLock::new(Some(dummy_manager)))), ..Default::default() } .channels(["test".into()]) @@ -226,12 +246,7 @@ mod should { let dummy_manager = SubscriptionManager::new(event_engine()); let subscription = SubscriptionBuilder { - subscription: Some(Arc::new(RwLock::new(Some(SubscriptionConfiguration { - inner: Arc::new(SubscriptionConfigurationRef { - subscription_manager: dummy_manager, - deserializer: None, - }), - })))), + subscription: Some(Arc::new(RwLock::new(Some(dummy_manager)))), ..Default::default() } .channels(["test".into()]) @@ -260,12 +275,7 @@ mod should { let dummy_manager = SubscriptionManager::new(event_engine()); let subscription = SubscriptionBuilder { - subscription: Some(Arc::new(RwLock::new(Some(SubscriptionConfiguration { - inner: Arc::new(SubscriptionConfigurationRef { - subscription_manager: dummy_manager, - deserializer: None, - }), - })))), + subscription: Some(Arc::new(RwLock::new(Some(dummy_manager)))), ..Default::default() } .channels(["test".into()]) diff --git a/src/lib.rs b/src/lib.rs index 0d21eef3..83315a01 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -180,6 +180,10 @@ pub use dx::publish; #[doc(inline)] pub use dx::subscribe; +#[cfg(feature = "presence")] +#[doc(inline)] +pub use dx::presence; + #[doc(inline)] pub use dx::{Keyset, PubNubClientBuilder, PubNubGenericClient}; diff --git a/src/providers/deserialization_serde.rs b/src/providers/deserialization_serde.rs index cbbbe4f2..08943ba0 100644 --- a/src/providers/deserialization_serde.rs +++ b/src/providers/deserialization_serde.rs @@ -32,13 +32,14 @@ use crate::{ /// [`Deserializer`]: ../trait.Deserializer.html /// [`serde`]: https://crates.io/crates/serde /// [`dx`]: ../dx/index.html -pub struct SerdeDeserializer; +#[derive(Debug, Clone)] +pub struct DeserializerSerde; -impl Deserializer for SerdeDeserializer -where - T: for<'de> serde::Deserialize<'de>, -{ - fn deserialize(&self, bytes: &[u8]) -> Result { +impl Deserializer for DeserializerSerde { + fn deserialize(&self, bytes: &[u8]) -> Result + where + T: for<'de> serde::Deserialize<'de>, + { serde_json::from_slice(bytes).map_err(|e| PubNubError::Deserialization { details: e.to_string(), }) @@ -47,8 +48,7 @@ where impl<'de, D> crate::core::Deserialize<'de> for D where - D: Send + Sync, - D: serde::Deserialize<'de>, + D: serde::Deserialize<'de> + Send + Sync, { type Type = D; @@ -72,7 +72,7 @@ mod should { #[test] fn deserialize() { - let sut = SerdeDeserializer; + let sut = DeserializerSerde; let result: Foo = sut.deserialize(&Vec::from("{\"bar\":\"baz\"}")).unwrap(); diff --git a/src/providers/futures_tokio.rs b/src/providers/futures_tokio.rs index 6b6a0bc3..95e09393 100644 --- a/src/providers/futures_tokio.rs +++ b/src/providers/futures_tokio.rs @@ -6,14 +6,14 @@ //! //! [`future_tokio` feature]: ../index.html#features -use crate::{core::runtime::Runtime, lib::alloc::boxed::Box}; +use crate::core::runtime::Runtime; /// Tokio-based `async` tasks spawner. #[derive(Copy, Clone, Debug)] -pub struct TokioRuntime; +pub struct RuntimeTokio; #[async_trait::async_trait] -impl Runtime for TokioRuntime { +impl Runtime for RuntimeTokio { fn spawn(&self, future: impl futures::Future + Send + 'static) where R: Send + 'static, diff --git a/src/transport/middleware.rs b/src/transport/middleware.rs index d8222bac..1bd3139c 100644 --- a/src/transport/middleware.rs +++ b/src/transport/middleware.rs @@ -1,7 +1,9 @@ //! PubNub middleware module. //! -//! This module contains the middleware that is used to add the required query parameters to the requests. -//! The middleware is used to add the `pnsdk`, `uuid`, `instanceid` and `requestid` query parameters to the requests. +//! This module contains the middleware that is used to add the required query +//! parameters to the requests. +//! The middleware is used to add the `pnsdk`, `uuid`, `instanceid` and +//! `requestid` query parameters to the requests. #[cfg(feature = "std")] use crate::{ @@ -36,7 +38,8 @@ use uuid::Uuid; /// PubNub middleware. /// /// This middleware is used to add the required query parameters to the requests. -/// The middleware is used to add the `pnsdk`, `uuid`, `instanceid` and `requestid` query parameters to the requests. +/// The middleware is used to add the `pnsdk`, `uuid`, `instanceid` and +/// `requestid` query parameters to the requests. /// /// The `pnsdk` query parameter is used to identify the SDK that is used to make the request. /// The `uuid` query parameter is used to identify the user that is making the request. @@ -126,7 +129,8 @@ impl PubNubMiddleware { req.query_parameters .insert("pnsdk".into(), format!("{}/{}", SDK_ID, PKG_VERSION)); req.query_parameters - .insert("uuid".into(), self.user_id.as_ref().into()); + .entry("uuid".into()) + .or_insert(self.user_id.as_ref().into()); if let Some(instance_id) = self.instance_id.as_deref() { req.query_parameters diff --git a/src/transport/reqwest.rs b/src/transport/reqwest.rs index 54ef6c83..e504b0a8 100644 --- a/src/transport/reqwest.rs +++ b/src/transport/reqwest.rs @@ -1,8 +1,8 @@ //! # Reqwest Transport Implementation //! //! This module contains the [`TransportReqwest`] struct. -//! It is used to send requests to the [`PubNub API`] using the [`reqwest`] crate. -//! It is intended to be used by the [`pubnub`] crate. +//! It is used to send requests to the [`PubNub API`] using the [`reqwest`] +//! crate. It is intended to be used by the [`pubnub`] crate. //! //! It requires the [`reqwest` feature] to be enabled. //! @@ -12,6 +12,15 @@ //! [`pubnub`]: ../index.html //! [`reqwest` feature]: ../index.html#features +#[cfg(any( + all(not(feature = "subscribe"), not(feature = "presence")), + not(feature = "std") +))] +use crate::dx::pubnub_client::PubNubClientDeserializerBuilder; + +#[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] +use crate::dx::pubnub_client::PubNubClientRuntimeBuilder; + use crate::{ core::{ error::PubNubError, transport::PUBNUB_DEFAULT_BASE_URL, utils::encoding::url_encode, @@ -34,9 +43,9 @@ use reqwest::{ StatusCode, }; -/// This struct is used to send requests to the [`PubNub API`] using the [`reqwest`] crate. -/// It is used as the transport type for the [`PubNubClient`]. -/// It is intended to be used by the [`pubnub`] crate. +/// This struct is used to send requests to the [`PubNub API`] using the +/// [`reqwest`] crate. It is used as the transport type for the +/// [`PubNubClient`]. It is intended to be used by the [`pubnub`] crate. /// /// [`PubNubClient`]: ../../dx/pubnub_client/struct.PubNubClientInstance.html /// [`PubNub API`]: https://www.pubnub.com/docs @@ -120,15 +129,14 @@ impl TransportReqwest { /// It is intended to be used by the [`pubnub`] crate. /// It is used by the [`PubNubClientBuilder`] to create a [`PubNubClient`]. /// - /// It provides a default [`reqwest`] client using [`reqwest::Client::default()`] - /// and a default hostname of `https://ps.pndsn.com`. + /// It provides a default [`reqwest`] client using + /// [`reqwest::Client::default()`] and a default hostname of `https://ps.pndsn.com`. /// /// # Example /// ``` /// use pubnub::transport::TransportReqwest; /// /// let transport = TransportReqwest::new(); - /// /// ``` /// /// [`PubNubClient`]: ../../dx/pubnub_client/struct.PubNubClientInstance.html @@ -234,11 +242,12 @@ fn create_result( }) } -impl PubNubClientBuilder { - /// Creates a new [`PubNubClientBuilder`] with the default [`TransportReqwest`] transport. - /// The default transport uses the [`reqwest`] crate to send requests to the [`PubNub API`]. - /// The default hostname is `https://ps.pndsn.com`. - /// The default [`reqwest`] client is created using [`reqwest::Client::default()`]. +impl PubNubClientBuilder { + /// Creates a new [`PubNubClientBuilder`] with the default + /// [`TransportReqwest`] transport. The default transport uses the + /// [`reqwest`] crate to send requests to the [`PubNub API`]. The default hostname is `https://ps.pndsn.com`. + /// The default [`reqwest`] client is created using + /// [`reqwest::Client::default()`]. /// /// # Examples /// ``` @@ -257,9 +266,43 @@ impl PubNubClientBuilder { /// [`TransportReqwest`]: ./struct.TransportReqwest.html /// [`reqwest`]: https://docs.rs/reqwest /// [`PubNub API`]: https://www.pubnub.com/docs - pub fn with_reqwest_transport() -> PubNubClientBuilder { - PubNubClientBuilder { - transport: Some(TransportReqwest::new()), + #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] + pub fn with_reqwest_transport() -> PubNubClientRuntimeBuilder { + PubNubClientRuntimeBuilder { + transport: TransportReqwest::new(), + } + } + + /// Creates a new [`PubNubClientBuilder`] with the default + /// [`TransportReqwest`] transport. The default transport uses the + /// [`reqwest`] crate to send requests to the [`PubNub API`]. The default hostname is `https://ps.pndsn.com`. + /// The default [`reqwest`] client is created using + /// [`reqwest::Client::default()`]. + /// + /// # Examples + /// ``` + /// use pubnub::{PubNubClientBuilder, Keyset}; + /// + /// let client = PubNubClientBuilder::with_reqwest_transport() + /// .with_keyset(Keyset { + /// subscribe_key: "sub-c-abc123", + /// publish_key: Some("pub-c-abc123"), + /// secret_key: None, + /// }) + /// .with_user_id("user-123") + /// .build(); + /// ``` + /// + /// [`TransportReqwest`]: ./struct.TransportReqwest.html + /// [`reqwest`]: https://docs.rs/reqwest + /// [`PubNub API`]: https://www.pubnub.com/docs + #[cfg(any( + all(not(feature = "subscribe"), not(feature = "presence")), + not(feature = "std") + ))] + pub fn with_reqwest_transport() -> PubNubClientDeserializerBuilder { + PubNubClientDeserializerBuilder { + transport: TransportReqwest::new(), } } } @@ -270,8 +313,8 @@ pub mod blocking { //! # Reqwest Transport Blocking Implementation //! //! This module contains the [`TransportReqwest`] struct. - //! It is used to send requests to the [`PubNub API`] using the [`reqwest`] crate. - //! It is intended to be used by the [`pubnub`] crate. + //! It is used to send requests to the [`PubNub API`] using the [`reqwest`] + //! crate. It is intended to be used by the [`pubnub`] crate. //! //! It requires the [`reqwest` and `blocking` feature] to be enabled. //! @@ -281,9 +324,15 @@ pub mod blocking { //! [`pubnub`]: ../index.html //! [`reqwest` feature]: ../index.html#features - use log::info; + #[cfg(any( + all(not(feature = "subscribe"), not(feature = "presence")), + not(feature = "std") + ))] + use crate::dx::pubnub_client::PubNubClientDeserializerBuilder; + + #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] + use crate::dx::pubnub_client::PubNubClientRuntimeBuilder; - use crate::transport::reqwest::extract_headers; use crate::{ core::{ transport::PUBNUB_DEFAULT_BASE_URL, PubNubError, TransportMethod, TransportRequest, @@ -293,13 +342,14 @@ pub mod blocking { boxed::Box, string::{String, ToString}, }, - transport::reqwest::{create_result, prepare_headers, prepare_url}, + transport::reqwest::{create_result, extract_headers, prepare_headers, prepare_url}, PubNubClientBuilder, }; + use log::info; - /// This struct is used to send requests to the [`PubNub API`] using the [`reqwest`] crate. - /// It is used as the transport type for the [`PubNubClient`]. - /// It is intended to be used by the [`pubnub`] crate. + /// This struct is used to send requests to the [`PubNub API`] using the + /// [`reqwest`] crate. It is used as the transport type for the + /// [`PubNubClient`]. It is intended to be used by the [`pubnub`] crate. /// /// It requires the [`reqwest` and `blocking` feature] to be enabled. /// @@ -377,10 +427,11 @@ pub mod blocking { /// Create a new [`TransportReqwest`] instance. /// It is used as the transport type for the [`PubNubClient`]. /// It is intended to be used by the [`pubnub`] crate. - /// It is used by the [`PubNubClientBuilder`] to create a [`PubNubClient`]. + /// It is used by the [`PubNubClientBuilder`] to create a + /// [`PubNubClient`]. /// - /// It provides a default [`reqwest`] client using [`reqwest::Client::default()`] - /// and a default hostname of `https://ps.pndsn.com`. + /// It provides a default [`reqwest`] client using + /// [`reqwest::Client::default()`] and a default hostname of `https://ps.pndsn.com`. /// /// # Example /// ``` @@ -428,11 +479,42 @@ pub mod blocking { } } - impl PubNubClientBuilder { - /// Creates a new [`PubNubClientBuilder`] with the default [`TransportReqwest`] transport. - /// The default transport uses the [`reqwest`] crate to send requests to the [`PubNub API`]. - /// The default hostname is `https://ps.pndsn.com`. - /// The default [`reqwest`] client is created using [`reqwest::Client::default()`]. + impl PubNubClientBuilder { + /// Creates a new [`PubNubClientBuilder`] with the default + /// [`TransportReqwest`] transport. The default transport uses + /// the [`reqwest`] crate to send requests to the [`PubNub API`]. The default hostname is `https://ps.pndsn.com`. + /// The default [`reqwest`] client is created using + /// [`reqwest::Client::default()`]. + /// + /// # Examples + /// ``` + /// use pubnub::{PubNubClientBuilder, Keyset}; + /// + /// let client = PubNubClientBuilder::with_reqwest_transport() + /// .with_keyset(Keyset { + /// subscribe_key: "sub-c-abc123", + /// publish_key: Some("pub-c-abc123"), + /// secret_key: None, + /// }) + /// .with_user_id("user-123") + /// .build(); + /// ``` + /// + /// [`TransportReqwest`]: ./struct.TransportReqwest.html + /// [`reqwest`]: https://docs.rs/reqwest + /// [`PubNub API`]: https://www.pubnub.com/docs + #[cfg(all(any(feature = "subscribe", feature = "presence"), feature = "std"))] + pub fn with_reqwest_blocking_transport() -> PubNubClientRuntimeBuilder { + PubNubClientRuntimeBuilder { + transport: TransportReqwest::new(), + } + } + + /// Creates a new [`PubNubClientBuilder`] with the default + /// [`TransportReqwest`] transport. The default transport uses + /// the [`reqwest`] crate to send requests to the [`PubNub API`]. The default hostname is `https://ps.pndsn.com`. + /// The default [`reqwest`] client is created using + /// [`reqwest::Client::default()`]. /// /// # Examples /// ``` @@ -451,9 +533,14 @@ pub mod blocking { /// [`TransportReqwest`]: ./struct.TransportReqwest.html /// [`reqwest`]: https://docs.rs/reqwest /// [`PubNub API`]: https://www.pubnub.com/docs - pub fn with_reqwest_blocking_transport() -> PubNubClientBuilder { - PubNubClientBuilder { - transport: Some(TransportReqwest::new()), + #[cfg(any( + all(not(feature = "subscribe"), not(feature = "presence")), + not(feature = "std") + ))] + pub fn with_reqwest_blocking_transport() -> PubNubClientDeserializerBuilder + { + PubNubClientDeserializerBuilder { + transport: TransportReqwest::new(), } } }