use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Serialize)]
pub struct CdpCommand {
pub id: u64,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
#[serde(rename = "sessionId", skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct RawCdpMessage {
pub id: Option<u64>,
pub method: Option<String>,
pub params: Option<Value>,
pub result: Option<Value>,
pub error: Option<CdpProtocolError>,
#[serde(rename = "sessionId")]
pub session_id: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CdpProtocolError {
pub code: i64,
pub message: String,
}
#[derive(Debug)]
pub struct CdpResponse {
pub id: u64,
pub result: Result<Value, CdpProtocolError>,
pub session_id: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CdpEvent {
pub method: String,
pub params: Value,
pub session_id: Option<String>,
}
pub enum MessageKind {
Response(CdpResponse),
Event(CdpEvent),
}
impl RawCdpMessage {
#[must_use]
pub fn classify(self) -> Option<MessageKind> {
if let Some(id) = self.id {
let result = if let Some(error) = self.error {
Err(error)
} else {
Ok(self.result.unwrap_or(Value::Null))
};
Some(MessageKind::Response(CdpResponse {
id,
result,
session_id: self.session_id,
}))
} else if let Some(method) = self.method {
Some(MessageKind::Event(CdpEvent {
method,
params: self.params.unwrap_or(Value::Null),
session_id: self.session_id,
}))
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn serialize_command_without_params_or_session() {
let cmd = CdpCommand {
id: 1,
method: "Browser.getVersion".into(),
params: None,
session_id: None,
};
let json: Value = serde_json::to_value(&cmd).unwrap();
assert_eq!(json["id"], 1);
assert_eq!(json["method"], "Browser.getVersion");
assert!(json.get("params").is_none());
assert!(json.get("sessionId").is_none());
}
#[test]
fn serialize_command_with_params() {
let cmd = CdpCommand {
id: 2,
method: "Page.navigate".into(),
params: Some(json!({"url": "https://example.com"})),
session_id: None,
};
let json: Value = serde_json::to_value(&cmd).unwrap();
assert_eq!(json["id"], 2);
assert_eq!(json["params"]["url"], "https://example.com");
assert!(json.get("sessionId").is_none());
}
#[test]
fn serialize_command_with_session_id() {
let cmd = CdpCommand {
id: 3,
method: "Runtime.evaluate".into(),
params: Some(json!({"expression": "1+1"})),
session_id: Some("session-abc".into()),
};
let json: Value = serde_json::to_value(&cmd).unwrap();
assert_eq!(json["sessionId"], "session-abc");
}
#[test]
fn deserialize_success_response() {
let raw: RawCdpMessage =
serde_json::from_str(r#"{"id": 1, "result": {"frameId": "abc"}}"#).unwrap();
assert_eq!(raw.id, Some(1));
assert!(raw.result.is_some());
assert!(raw.error.is_none());
assert!(raw.method.is_none());
}
#[test]
fn deserialize_error_response() {
let raw: RawCdpMessage =
serde_json::from_str(r#"{"id": 2, "error": {"code": -32000, "message": "Not found"}}"#)
.unwrap();
assert_eq!(raw.id, Some(2));
assert!(raw.error.is_some());
let err = raw.error.unwrap();
assert_eq!(err.code, -32000);
assert_eq!(err.message, "Not found");
}
#[test]
fn deserialize_event() {
let raw: RawCdpMessage = serde_json::from_str(
r#"{"method": "Page.loadEventFired", "params": {"timestamp": 123.456}}"#,
)
.unwrap();
assert!(raw.id.is_none());
assert_eq!(raw.method.as_deref(), Some("Page.loadEventFired"));
assert!(raw.params.is_some());
}
#[test]
fn deserialize_session_scoped_event() {
let raw: RawCdpMessage = serde_json::from_str(
r#"{"method": "DOM.documentUpdated", "params": {}, "sessionId": "sess-1"}"#,
)
.unwrap();
assert_eq!(raw.session_id.as_deref(), Some("sess-1"));
}
#[test]
fn deserialize_session_scoped_response() {
let raw: RawCdpMessage =
serde_json::from_str(r#"{"id": 5, "result": {}, "sessionId": "sess-2"}"#).unwrap();
assert_eq!(raw.id, Some(5));
assert_eq!(raw.session_id.as_deref(), Some("sess-2"));
}
#[test]
fn classify_response() {
let raw: RawCdpMessage =
serde_json::from_str(r#"{"id": 1, "result": {"ok": true}}"#).unwrap();
let kind = raw.classify();
assert!(matches!(kind, Some(MessageKind::Response(_))));
if let Some(MessageKind::Response(resp)) = kind {
assert_eq!(resp.id, 1);
assert!(resp.result.is_ok());
}
}
#[test]
fn classify_error_response() {
let raw: RawCdpMessage = serde_json::from_str(
r#"{"id": 2, "error": {"code": -32600, "message": "Invalid request"}}"#,
)
.unwrap();
let kind = raw.classify();
assert!(matches!(kind, Some(MessageKind::Response(_))));
if let Some(MessageKind::Response(resp)) = kind {
assert_eq!(resp.id, 2);
assert!(resp.result.is_err());
let err = resp.result.unwrap_err();
assert_eq!(err.code, -32600);
}
}
#[test]
fn classify_event() {
let raw: RawCdpMessage = serde_json::from_str(
r#"{"method": "Network.requestWillBeSent", "params": {"requestId": "r1"}}"#,
)
.unwrap();
let kind = raw.classify();
assert!(matches!(kind, Some(MessageKind::Event(_))));
if let Some(MessageKind::Event(event)) = kind {
assert_eq!(event.method, "Network.requestWillBeSent");
assert_eq!(event.params["requestId"], "r1");
}
}
#[test]
fn classify_unclassifiable_returns_none() {
let raw: RawCdpMessage = serde_json::from_str(r"{}").unwrap();
assert!(raw.classify().is_none());
}
#[test]
fn classify_response_without_result_yields_null() {
let raw: RawCdpMessage = serde_json::from_str(r#"{"id": 10}"#).unwrap();
if let Some(MessageKind::Response(resp)) = raw.classify() {
assert_eq!(resp.result.unwrap(), Value::Null);
} else {
panic!("expected response");
}
}
#[test]
fn classify_event_without_params_yields_null() {
let raw: RawCdpMessage =
serde_json::from_str(r#"{"method": "Page.frameNavigated"}"#).unwrap();
if let Some(MessageKind::Event(event)) = raw.classify() {
assert_eq!(event.params, Value::Null);
} else {
panic!("expected event");
}
}
#[test]
fn message_ids_are_unique_and_monotonic() {
use std::sync::atomic::{AtomicU64, Ordering};
let counter = AtomicU64::new(1);
let id1 = counter.fetch_add(1, Ordering::Relaxed);
let id2 = counter.fetch_add(1, Ordering::Relaxed);
let id3 = counter.fetch_add(1, Ordering::Relaxed);
assert!(id2 > id1);
assert!(id3 > id2);
}
}