Skip to content

Commit 81abc30

Browse files
committed
feat: implement session management methods
Implement WebSocket-specific session management methods: - set_heartbeat(interval): Enable heartbeats with specified interval (10-3600 seconds) - disable_heartbeat(): Disable heartbeat messages - hello(client_name, client_version): Send client identification to server Changes: - Add PUBLIC_SET_HEARTBEAT, PUBLIC_DISABLE_HEARTBEAT constants - Add request builder methods for session management - Implement client methods with proper error handling - Add 8 unit tests for request builders Closes #14
1 parent 603471f commit 81abc30

File tree

4 files changed

+293
-1
lines changed

4 files changed

+293
-1
lines changed

src/client.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,108 @@ impl DeribitWebSocketClient {
307307
self.send_request(request).await
308308
}
309309

310+
/// Enable heartbeat with specified interval
311+
///
312+
/// The server will send a heartbeat message every `interval` seconds.
313+
/// If heartbeat is enabled, the server will also send `test_request` notifications
314+
/// which the client should respond to with `public/test` to keep the connection alive.
315+
///
316+
/// # Arguments
317+
///
318+
/// * `interval` - Heartbeat interval in seconds (10-3600)
319+
///
320+
/// # Returns
321+
///
322+
/// Returns `"ok"` on success
323+
///
324+
/// # Errors
325+
///
326+
/// Returns an error if the request fails or the interval is invalid
327+
pub async fn set_heartbeat(&self, interval: u64) -> Result<String, WebSocketError> {
328+
let request = {
329+
let mut builder = self.request_builder.lock().await;
330+
builder.build_set_heartbeat_request(interval)
331+
};
332+
333+
let response = self.send_request(request).await?;
334+
335+
match response.result {
336+
JsonRpcResult::Success { result } => {
337+
result.as_str().map(String::from).ok_or_else(|| {
338+
WebSocketError::InvalidMessage(
339+
"Expected string result from set_heartbeat".to_string(),
340+
)
341+
})
342+
}
343+
JsonRpcResult::Error { error } => {
344+
Err(WebSocketError::ApiError(error.code, error.message))
345+
}
346+
}
347+
}
348+
349+
/// Disable heartbeat
350+
///
351+
/// Stops the server from sending heartbeat messages and `test_request` notifications.
352+
///
353+
/// # Returns
354+
///
355+
/// Returns `"ok"` on success
356+
///
357+
/// # Errors
358+
///
359+
/// Returns an error if the request fails
360+
pub async fn disable_heartbeat(&self) -> Result<String, WebSocketError> {
361+
let request = {
362+
let mut builder = self.request_builder.lock().await;
363+
builder.build_disable_heartbeat_request()
364+
};
365+
366+
let response = self.send_request(request).await?;
367+
368+
match response.result {
369+
JsonRpcResult::Success { result } => {
370+
result.as_str().map(String::from).ok_or_else(|| {
371+
WebSocketError::InvalidMessage(
372+
"Expected string result from disable_heartbeat".to_string(),
373+
)
374+
})
375+
}
376+
JsonRpcResult::Error { error } => {
377+
Err(WebSocketError::ApiError(error.code, error.message))
378+
}
379+
}
380+
}
381+
382+
/// Send client identification to the server
383+
///
384+
/// This method identifies the client to the server with its name and version.
385+
/// It's recommended to call this after connecting to provide debugging information.
386+
///
387+
/// # Arguments
388+
///
389+
/// * `client_name` - Name of the client application
390+
/// * `client_version` - Version of the client application
391+
///
392+
/// # Returns
393+
///
394+
/// Returns a JSON response containing the API version information
395+
///
396+
/// # Errors
397+
///
398+
/// Returns an error if the request fails
399+
pub async fn hello(
400+
&self,
401+
client_name: &str,
402+
client_version: &str,
403+
) -> Result<JsonRpcResponse, WebSocketError> {
404+
let request = {
405+
let mut builder = self.request_builder.lock().await;
406+
builder.build_hello_request(client_name, client_version)
407+
};
408+
409+
self.send_request(request).await
410+
}
411+
310412
/// Place mass quotes
311413
pub async fn mass_quote(
312414
&self,

src/constants.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ pub mod methods {
3333
/// Private unsubscribe from all channels
3434
pub const PRIVATE_UNSUBSCRIBE_ALL: &str = "private/unsubscribe_all";
3535

36+
// Session management
37+
/// Set heartbeat interval
38+
pub const PUBLIC_SET_HEARTBEAT: &str = "public/set_heartbeat";
39+
/// Disable heartbeat
40+
pub const PUBLIC_DISABLE_HEARTBEAT: &str = "public/disable_heartbeat";
41+
3642
// Market data
3743
/// Get ticker information
3844
pub const PUBLIC_GET_TICKER: &str = "public/ticker";

src/message/request.rs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,69 @@ impl RequestBuilder {
9797

9898
/// Build test request
9999
pub fn build_test_request(&mut self) -> JsonRpcRequest {
100-
self.build_request("public/test", None)
100+
self.build_request(crate::constants::methods::PUBLIC_TEST, None)
101+
}
102+
103+
/// Build set_heartbeat request
104+
///
105+
/// Enables heartbeat with specified interval. The server will send a heartbeat
106+
/// message every `interval` seconds, and expects a response within the same interval.
107+
///
108+
/// # Arguments
109+
///
110+
/// * `interval` - Heartbeat interval in seconds (10-3600)
111+
///
112+
/// # Returns
113+
///
114+
/// A JSON-RPC request for setting the heartbeat interval
115+
pub fn build_set_heartbeat_request(&mut self, interval: u64) -> JsonRpcRequest {
116+
let params = serde_json::json!({
117+
"interval": interval
118+
});
119+
self.build_request(
120+
crate::constants::methods::PUBLIC_SET_HEARTBEAT,
121+
Some(params),
122+
)
123+
}
124+
125+
/// Build disable_heartbeat request
126+
///
127+
/// Disables heartbeat messages. The server will stop sending heartbeat messages
128+
/// and test_request notifications.
129+
///
130+
/// # Returns
131+
///
132+
/// A JSON-RPC request for disabling heartbeats
133+
pub fn build_disable_heartbeat_request(&mut self) -> JsonRpcRequest {
134+
self.build_request(
135+
crate::constants::methods::PUBLIC_DISABLE_HEARTBEAT,
136+
Some(serde_json::json!({})),
137+
)
138+
}
139+
140+
/// Build hello request
141+
///
142+
/// Sends client identification to the server. This is used for client tracking
143+
/// and debugging purposes.
144+
///
145+
/// # Arguments
146+
///
147+
/// * `client_name` - Name of the client application
148+
/// * `client_version` - Version of the client application
149+
///
150+
/// # Returns
151+
///
152+
/// A JSON-RPC request for client identification
153+
pub fn build_hello_request(
154+
&mut self,
155+
client_name: &str,
156+
client_version: &str,
157+
) -> JsonRpcRequest {
158+
let params = serde_json::json!({
159+
"client_name": client_name,
160+
"client_version": client_version
161+
});
162+
self.build_request(crate::constants::methods::PUBLIC_HELLO, Some(params))
101163
}
102164

103165
/// Build get time request

tests/unit/message.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,125 @@ fn test_request_with_empty_channels() {
147147
assert_eq!(channels_array.len(), 0);
148148
}
149149
}
150+
151+
// =============================================================================
152+
// Session management request tests (Issue #14)
153+
// =============================================================================
154+
155+
#[test]
156+
fn test_request_builder_set_heartbeat() {
157+
let mut builder = RequestBuilder::new();
158+
let request = builder.build_set_heartbeat_request(30);
159+
160+
assert_eq!(request.method, "public/set_heartbeat");
161+
assert_eq!(request.jsonrpc, "2.0");
162+
assert!(request.id.is_number());
163+
164+
if let Some(params) = request.params {
165+
let params_obj = params.as_object().unwrap();
166+
assert_eq!(params_obj["interval"], 30);
167+
} else {
168+
panic!("set_heartbeat request should have params");
169+
}
170+
}
171+
172+
#[test]
173+
fn test_request_builder_set_heartbeat_custom_interval() {
174+
let mut builder = RequestBuilder::new();
175+
let request = builder.build_set_heartbeat_request(60);
176+
177+
if let Some(params) = request.params {
178+
let params_obj = params.as_object().unwrap();
179+
assert_eq!(params_obj["interval"], 60);
180+
} else {
181+
panic!("set_heartbeat request should have params");
182+
}
183+
}
184+
185+
#[test]
186+
fn test_request_builder_disable_heartbeat() {
187+
let mut builder = RequestBuilder::new();
188+
let request = builder.build_disable_heartbeat_request();
189+
190+
assert_eq!(request.method, "public/disable_heartbeat");
191+
assert_eq!(request.jsonrpc, "2.0");
192+
assert!(request.id.is_number());
193+
assert!(request.params.is_some());
194+
}
195+
196+
#[test]
197+
fn test_request_builder_hello() {
198+
let mut builder = RequestBuilder::new();
199+
let request = builder.build_hello_request("test-client", "1.0.0");
200+
201+
assert_eq!(request.method, "public/hello");
202+
assert_eq!(request.jsonrpc, "2.0");
203+
assert!(request.id.is_number());
204+
205+
if let Some(params) = request.params {
206+
let params_obj = params.as_object().unwrap();
207+
assert_eq!(params_obj["client_name"], "test-client");
208+
assert_eq!(params_obj["client_version"], "1.0.0");
209+
} else {
210+
panic!("hello request should have params");
211+
}
212+
}
213+
214+
#[test]
215+
fn test_request_builder_hello_custom_values() {
216+
let mut builder = RequestBuilder::new();
217+
let request = builder.build_hello_request("deribit-websocket", "0.2.0");
218+
219+
if let Some(params) = request.params {
220+
let params_obj = params.as_object().unwrap();
221+
assert_eq!(params_obj["client_name"], "deribit-websocket");
222+
assert_eq!(params_obj["client_version"], "0.2.0");
223+
} else {
224+
panic!("hello request should have params");
225+
}
226+
}
227+
228+
#[test]
229+
fn test_request_builder_set_heartbeat_serialization() {
230+
let mut builder = RequestBuilder::new();
231+
let request = builder.build_set_heartbeat_request(30);
232+
233+
let serialized = serde_json::to_string(&request).unwrap();
234+
let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
235+
236+
assert_eq!(parsed["jsonrpc"], "2.0");
237+
assert_eq!(parsed["method"], "public/set_heartbeat");
238+
assert!(parsed["id"].is_number());
239+
assert_eq!(parsed["params"]["interval"], 30);
240+
}
241+
242+
#[test]
243+
fn test_request_builder_hello_serialization() {
244+
let mut builder = RequestBuilder::new();
245+
let request = builder.build_hello_request("my-app", "2.0.0");
246+
247+
let serialized = serde_json::to_string(&request).unwrap();
248+
let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
249+
250+
assert_eq!(parsed["jsonrpc"], "2.0");
251+
assert_eq!(parsed["method"], "public/hello");
252+
assert!(parsed["id"].is_number());
253+
assert_eq!(parsed["params"]["client_name"], "my-app");
254+
assert_eq!(parsed["params"]["client_version"], "2.0.0");
255+
}
256+
257+
#[test]
258+
fn test_request_builder_incremental_ids_session_methods() {
259+
let mut builder = RequestBuilder::new();
260+
261+
let req1 = builder.build_set_heartbeat_request(30);
262+
let req2 = builder.build_disable_heartbeat_request();
263+
let req3 = builder.build_hello_request("test", "1.0");
264+
265+
let id1 = req1.id.as_u64().unwrap();
266+
let id2 = req2.id.as_u64().unwrap();
267+
let id3 = req3.id.as_u64().unwrap();
268+
269+
assert_eq!(id2, id1 + 1);
270+
assert_eq!(id3, id2 + 1);
271+
}

0 commit comments

Comments
 (0)