1use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8#[derive(Debug, Clone, Deserialize)]
10pub struct CdpMessage {
11 pub id: Option<i64>,
12 pub method: String,
13 #[serde(default)]
14 pub params: Option<Value>,
15 #[serde(default, rename = "sessionId")]
17 pub session_id: Option<String>,
18}
19
20#[derive(Debug, Serialize)]
22pub struct CdpResponse {
23 pub id: Option<i64>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub result: Option<Value>,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub error: Option<CdpError>,
28}
29
30#[derive(Debug, Clone, Serialize)]
32pub struct CdpError {
33 pub code: i64,
34 pub message: String,
35}
36
37#[derive(Debug)]
39pub enum SessionError {
40 Closed,
42 Io,
44}
45
46#[derive(Debug, Clone, Serialize)]
48pub struct CdpEvent {
49 pub method: String,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub params: Option<Value>,
52}
53
54pub const ERR_INVALID_REQUEST: i64 = -32600;
56pub const ERR_METHOD_NOT_FOUND: i64 = -32601;
57#[allow(dead_code)]
58pub const ERR_INVALID_PARAMS: i64 = -32602;
59#[allow(dead_code)]
60pub const ERR_INTERNAL: i64 = -32603;
61#[allow(dead_code)]
62pub const ERR_PARSE_ERROR: i64 = -32700;
63
64pub fn parse_message(raw: &str) -> Option<CdpMessage> {
65 serde_json::from_str(raw).ok()
66}
67
68pub fn serialize_response(resp: &CdpResponse) -> String {
69 serde_json::to_string(resp).unwrap_or_else(|_| {
70 r#"{"id":null,"error":{"code":-32700,"message":"serialize error"}}"#.into()
71 })
72}
73
74pub fn serialize_event(ev: &CdpEvent) -> String {
75 serde_json::to_string(ev).unwrap_or_else(|_| "{}".into())
76}
77
78pub fn ok_response(id: Option<i64>, result: Value) -> CdpResponse {
79 CdpResponse {
80 id,
81 result: Some(result),
82 error: None,
83 }
84}
85
86pub fn error_response(id: Option<i64>, code: i64, message: impl Into<String>) -> CdpResponse {
87 CdpResponse {
88 id,
89 result: None,
90 error: Some(CdpError {
91 code,
92 message: message.into(),
93 }),
94 }
95}
96
97pub fn ok_empty(id: Option<i64>) -> CdpResponse {
98 ok_response(id, serde_json::json!({}))
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use serde_json::json;
105
106 #[test]
108 fn parse_valid_json_returns_some() {
109 let msg = parse_message(r#"{"id":1,"method":"Page.navigate"}"#);
110 assert!(msg.is_some());
111 let m = msg.unwrap();
112 assert_eq!(m.id, Some(1));
113 assert_eq!(m.method, "Page.navigate");
114 }
115
116 #[test]
118 fn parse_invalid_json_returns_none() {
119 assert!(parse_message("{not json}").is_none());
120 assert!(parse_message("").is_none());
121 }
122
123 #[test]
125 fn parse_null_id_is_none() {
126 let msg = parse_message(r#"{"id":null,"method":"Page.reload"}"#).unwrap();
127 assert_eq!(msg.id, None);
128 }
129
130 #[test]
132 fn parse_zero_id_is_some_zero() {
133 let msg = parse_message(r#"{"id":0,"method":"Page.reload"}"#).unwrap();
134 assert_eq!(msg.id, Some(0));
135 }
136
137 #[test]
141 fn parse_session_id() {
142 let msg = parse_message(r#"{"id":5,"method":"Runtime.evaluate","sessionId":"sess-abc"}"#)
143 .unwrap();
144 assert_eq!(msg.session_id, Some("sess-abc".into()));
145 let snake = parse_message(r#"{"id":5,"method":"Runtime.evaluate","session_id":"x"}"#)
147 .unwrap();
148 assert_eq!(snake.session_id, None);
149 }
150
151 #[test]
153 fn parse_nested_params() {
154 let msg = parse_message(
155 r#"{"id":2,"method":"Page.navigate","params":{"url":"https://example.com","transitionType":"link"}}"#,
156 )
157 .unwrap();
158 let params = msg.params.unwrap();
159 assert_eq!(params["url"], "https://example.com");
160 assert_eq!(params["transitionType"], "link");
161 }
162
163 #[test]
165 fn parse_array_params() {
166 let msg =
167 parse_message(r#"{"id":3,"method":"DOM.querySelectorAll","params":["div","span"]}"#)
168 .unwrap();
169 let params = msg.params.unwrap();
170 assert!(params.is_array());
171 assert_eq!(params.as_array().unwrap().len(), 2);
172 }
173
174 #[test]
176 fn serialize_response_result_only() {
177 let resp = ok_response(Some(1), json!({"frameId": "f1"}));
178 let s = serialize_response(&resp);
179 let v: Value = serde_json::from_str(&s).unwrap();
180 assert!(v.get("result").is_some());
181 assert!(v.get("error").is_none());
182 assert_eq!(v["id"], 1);
183 }
184
185 #[test]
187 fn serialize_response_error_only() {
188 let resp = error_response(Some(2), ERR_METHOD_NOT_FOUND, "not found");
189 let s = serialize_response(&resp);
190 let v: Value = serde_json::from_str(&s).unwrap();
191 assert!(v.get("error").is_some());
192 assert!(v.get("result").is_none());
193 }
194
195 #[test]
197 fn serialize_response_produces_valid_json() {
198 let resp = ok_response(Some(42), json!({"data": true}));
199 let s = serialize_response(&resp);
200 assert!(serde_json::from_str::<Value>(&s).is_ok());
201 }
202
203 #[test]
205 fn serialize_event_with_params() {
206 let ev = CdpEvent {
207 method: "Page.frameNavigated".into(),
208 params: Some(json!({"frameId": "f1"})),
209 };
210 let s = serialize_event(&ev);
211 let v: Value = serde_json::from_str(&s).unwrap();
212 assert_eq!(v["method"], "Page.frameNavigated");
213 assert!(v.get("params").is_some());
214 }
215
216 #[test]
218 fn serialize_event_method_only() {
219 let ev = CdpEvent {
220 method: "Page.domContentEventFired".into(),
221 params: None,
222 };
223 let s = serialize_event(&ev);
224 let v: Value = serde_json::from_str(&s).unwrap();
225 assert_eq!(v["method"], "Page.domContentEventFired");
226 assert!(v.get("params").is_none());
227 }
228
229 #[test]
231 fn ok_response_has_result_no_error() {
232 let resp = ok_response(Some(1), json!({"ok": true}));
233 assert!(resp.result.is_some());
234 assert!(resp.error.is_none());
235 }
236
237 #[test]
239 fn error_response_has_error_no_result() {
240 let resp = error_response(Some(1), -32600, "invalid");
241 assert!(resp.error.is_some());
242 assert!(resp.result.is_none());
243 }
244
245 #[test]
247 fn ok_empty_returns_empty_object() {
248 let resp = ok_empty(Some(1));
249 assert_eq!(resp.result, Some(json!({})));
250 assert!(resp.error.is_none());
251 }
252
253 #[test]
255 fn cdp_error_debug_format() {
256 let err = CdpError {
257 code: -32601,
258 message: "Method not found".into(),
259 };
260 let dbg = format!("{:?}", err);
261 assert!(dbg.contains("-32601"));
262 assert!(dbg.contains("Method not found"));
263 }
264
265 #[test]
267 fn cdp_response_large_id() {
268 let resp = ok_response(Some(i64::MAX), json!({}));
269 let s = serialize_response(&resp);
270 let v: Value = serde_json::from_str(&s).unwrap();
271 assert_eq!(v["id"].as_i64(), Some(i64::MAX));
272 }
273
274 #[test]
276 fn cdp_response_negative_id() {
277 let resp = ok_response(Some(-100), json!({}));
278 let s = serialize_response(&resp);
279 let v: Value = serde_json::from_str(&s).unwrap();
280 assert_eq!(v["id"].as_i64(), Some(-100));
281 }
282
283 #[test]
285 fn parse_unicode_method() {
286 let msg = parse_message(r#"{"id":1,"method":"Page.日本語テスト"}"#).unwrap();
287 assert_eq!(msg.method, "Page.日本語テスト");
288 }
289
290 #[test]
292 fn parse_extra_fields_ignored() {
293 let msg =
294 parse_message(r#"{"id":1,"method":"Page.reload","extra":"ignored","another":123}"#)
295 .unwrap();
296 assert_eq!(msg.id, Some(1));
297 assert_eq!(msg.method, "Page.reload");
298 }
299
300 #[test]
302 fn session_error_debug_format() {
303 let closed_dbg = format!("{:?}", SessionError::Closed);
304 assert!(closed_dbg.contains("Closed"));
305 let io_dbg = format!("{:?}", SessionError::Io);
306 assert!(io_dbg.contains("Io"));
307 }
308
309 #[test]
311 fn error_code_constants() {
312 assert_eq!(ERR_INVALID_REQUEST, -32600);
313 assert_eq!(ERR_METHOD_NOT_FOUND, -32601);
314 }
315
316 #[test]
318 fn ok_response_serialize_contains_result() {
319 let resp = ok_response(Some(1), json!({"value": 42}));
320 let s = serialize_response(&resp);
321 assert!(s.contains("\"result\""));
322 }
323
324 #[test]
326 fn error_response_serialize_contains_error() {
327 let resp = error_response(Some(1), ERR_INVALID_REQUEST, "bad");
328 let s = serialize_response(&resp);
329 assert!(s.contains("\"error\""));
330 }
331}