use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Deserialize)]
pub struct CdpMessage {
pub id: Option<i64>,
pub method: String,
#[serde(default)]
pub params: Option<Value>,
#[serde(default, rename = "sessionId")]
pub session_id: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct CdpResponse {
pub id: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<CdpError>,
}
#[derive(Debug, Clone, Serialize)]
pub struct CdpError {
pub code: i64,
pub message: String,
}
#[derive(Debug)]
pub enum SessionError {
Closed,
Io,
}
#[derive(Debug, Clone, Serialize)]
pub struct CdpEvent {
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
}
pub const ERR_INVALID_REQUEST: i64 = -32600;
pub const ERR_METHOD_NOT_FOUND: i64 = -32601;
#[allow(dead_code)]
pub const ERR_INVALID_PARAMS: i64 = -32602;
#[allow(dead_code)]
pub const ERR_INTERNAL: i64 = -32603;
#[allow(dead_code)]
pub const ERR_PARSE_ERROR: i64 = -32700;
pub fn parse_message(raw: &str) -> Option<CdpMessage> {
serde_json::from_str(raw).ok()
}
pub fn serialize_response(resp: &CdpResponse) -> String {
serde_json::to_string(resp).unwrap_or_else(|_| {
r#"{"id":null,"error":{"code":-32700,"message":"serialize error"}}"#.into()
})
}
pub fn serialize_event(ev: &CdpEvent) -> String {
serde_json::to_string(ev).unwrap_or_else(|_| "{}".into())
}
pub fn ok_response(id: Option<i64>, result: Value) -> CdpResponse {
CdpResponse {
id,
result: Some(result),
error: None,
}
}
pub fn error_response(id: Option<i64>, code: i64, message: impl Into<String>) -> CdpResponse {
CdpResponse {
id,
result: None,
error: Some(CdpError {
code,
message: message.into(),
}),
}
}
pub fn ok_empty(id: Option<i64>) -> CdpResponse {
ok_response(id, serde_json::json!({}))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn parse_valid_json_returns_some() {
let msg = parse_message(r#"{"id":1,"method":"Page.navigate"}"#);
assert!(msg.is_some());
let m = msg.unwrap();
assert_eq!(m.id, Some(1));
assert_eq!(m.method, "Page.navigate");
}
#[test]
fn parse_invalid_json_returns_none() {
assert!(parse_message("{not json}").is_none());
assert!(parse_message("").is_none());
}
#[test]
fn parse_null_id_is_none() {
let msg = parse_message(r#"{"id":null,"method":"Page.reload"}"#).unwrap();
assert_eq!(msg.id, None);
}
#[test]
fn parse_zero_id_is_some_zero() {
let msg = parse_message(r#"{"id":0,"method":"Page.reload"}"#).unwrap();
assert_eq!(msg.id, Some(0));
}
#[test]
fn parse_session_id() {
let msg = parse_message(r#"{"id":5,"method":"Runtime.evaluate","sessionId":"sess-abc"}"#)
.unwrap();
assert_eq!(msg.session_id, Some("sess-abc".into()));
let snake = parse_message(r#"{"id":5,"method":"Runtime.evaluate","session_id":"x"}"#)
.unwrap();
assert_eq!(snake.session_id, None);
}
#[test]
fn parse_nested_params() {
let msg = parse_message(
r#"{"id":2,"method":"Page.navigate","params":{"url":"https://example.com","transitionType":"link"}}"#,
)
.unwrap();
let params = msg.params.unwrap();
assert_eq!(params["url"], "https://example.com");
assert_eq!(params["transitionType"], "link");
}
#[test]
fn parse_array_params() {
let msg =
parse_message(r#"{"id":3,"method":"DOM.querySelectorAll","params":["div","span"]}"#)
.unwrap();
let params = msg.params.unwrap();
assert!(params.is_array());
assert_eq!(params.as_array().unwrap().len(), 2);
}
#[test]
fn serialize_response_result_only() {
let resp = ok_response(Some(1), json!({"frameId": "f1"}));
let s = serialize_response(&resp);
let v: Value = serde_json::from_str(&s).unwrap();
assert!(v.get("result").is_some());
assert!(v.get("error").is_none());
assert_eq!(v["id"], 1);
}
#[test]
fn serialize_response_error_only() {
let resp = error_response(Some(2), ERR_METHOD_NOT_FOUND, "not found");
let s = serialize_response(&resp);
let v: Value = serde_json::from_str(&s).unwrap();
assert!(v.get("error").is_some());
assert!(v.get("result").is_none());
}
#[test]
fn serialize_response_produces_valid_json() {
let resp = ok_response(Some(42), json!({"data": true}));
let s = serialize_response(&resp);
assert!(serde_json::from_str::<Value>(&s).is_ok());
}
#[test]
fn serialize_event_with_params() {
let ev = CdpEvent {
method: "Page.frameNavigated".into(),
params: Some(json!({"frameId": "f1"})),
};
let s = serialize_event(&ev);
let v: Value = serde_json::from_str(&s).unwrap();
assert_eq!(v["method"], "Page.frameNavigated");
assert!(v.get("params").is_some());
}
#[test]
fn serialize_event_method_only() {
let ev = CdpEvent {
method: "Page.domContentEventFired".into(),
params: None,
};
let s = serialize_event(&ev);
let v: Value = serde_json::from_str(&s).unwrap();
assert_eq!(v["method"], "Page.domContentEventFired");
assert!(v.get("params").is_none());
}
#[test]
fn ok_response_has_result_no_error() {
let resp = ok_response(Some(1), json!({"ok": true}));
assert!(resp.result.is_some());
assert!(resp.error.is_none());
}
#[test]
fn error_response_has_error_no_result() {
let resp = error_response(Some(1), -32600, "invalid");
assert!(resp.error.is_some());
assert!(resp.result.is_none());
}
#[test]
fn ok_empty_returns_empty_object() {
let resp = ok_empty(Some(1));
assert_eq!(resp.result, Some(json!({})));
assert!(resp.error.is_none());
}
#[test]
fn cdp_error_debug_format() {
let err = CdpError {
code: -32601,
message: "Method not found".into(),
};
let dbg = format!("{:?}", err);
assert!(dbg.contains("-32601"));
assert!(dbg.contains("Method not found"));
}
#[test]
fn cdp_response_large_id() {
let resp = ok_response(Some(i64::MAX), json!({}));
let s = serialize_response(&resp);
let v: Value = serde_json::from_str(&s).unwrap();
assert_eq!(v["id"].as_i64(), Some(i64::MAX));
}
#[test]
fn cdp_response_negative_id() {
let resp = ok_response(Some(-100), json!({}));
let s = serialize_response(&resp);
let v: Value = serde_json::from_str(&s).unwrap();
assert_eq!(v["id"].as_i64(), Some(-100));
}
#[test]
fn parse_unicode_method() {
let msg = parse_message(r#"{"id":1,"method":"Page.日本語テスト"}"#).unwrap();
assert_eq!(msg.method, "Page.日本語テスト");
}
#[test]
fn parse_extra_fields_ignored() {
let msg =
parse_message(r#"{"id":1,"method":"Page.reload","extra":"ignored","another":123}"#)
.unwrap();
assert_eq!(msg.id, Some(1));
assert_eq!(msg.method, "Page.reload");
}
#[test]
fn session_error_debug_format() {
let closed_dbg = format!("{:?}", SessionError::Closed);
assert!(closed_dbg.contains("Closed"));
let io_dbg = format!("{:?}", SessionError::Io);
assert!(io_dbg.contains("Io"));
}
#[test]
fn error_code_constants() {
assert_eq!(ERR_INVALID_REQUEST, -32600);
assert_eq!(ERR_METHOD_NOT_FOUND, -32601);
}
#[test]
fn ok_response_serialize_contains_result() {
let resp = ok_response(Some(1), json!({"value": 42}));
let s = serialize_response(&resp);
assert!(s.contains("\"result\""));
}
#[test]
fn error_response_serialize_contains_error() {
let resp = error_response(Some(1), ERR_INVALID_REQUEST, "bad");
let s = serialize_response(&resp);
assert!(s.contains("\"error\""));
}
}