1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Serialize)]
6pub struct CdpCommand {
7 pub id: u64,
9 pub method: String,
11 #[serde(skip_serializing_if = "Option::is_none")]
13 pub params: Option<Value>,
14 #[serde(rename = "sessionId", skip_serializing_if = "Option::is_none")]
16 pub session_id: Option<String>,
17}
18
19#[derive(Debug, Deserialize)]
25pub struct RawCdpMessage {
26 pub id: Option<u64>,
28 pub method: Option<String>,
30 pub params: Option<Value>,
32 pub result: Option<Value>,
34 pub error: Option<CdpProtocolError>,
36 #[serde(rename = "sessionId")]
38 pub session_id: Option<String>,
39}
40
41#[derive(Debug, Clone, Deserialize)]
43pub struct CdpProtocolError {
44 pub code: i64,
46 pub message: String,
48}
49
50#[derive(Debug)]
52pub struct CdpResponse {
53 pub id: u64,
55 pub result: Result<Value, CdpProtocolError>,
57 pub session_id: Option<String>,
59}
60
61#[derive(Debug, Clone)]
63pub struct CdpEvent {
64 pub method: String,
66 pub params: Value,
68 pub session_id: Option<String>,
70}
71
72pub enum MessageKind {
74 Response(CdpResponse),
76 Event(CdpEvent),
78}
79
80impl RawCdpMessage {
81 #[must_use]
87 pub fn classify(self) -> Option<MessageKind> {
88 if let Some(id) = self.id {
89 let result = if let Some(error) = self.error {
90 Err(error)
91 } else {
92 Ok(self.result.unwrap_or(Value::Null))
93 };
94 Some(MessageKind::Response(CdpResponse {
95 id,
96 result,
97 session_id: self.session_id,
98 }))
99 } else if let Some(method) = self.method {
100 Some(MessageKind::Event(CdpEvent {
101 method,
102 params: self.params.unwrap_or(Value::Null),
103 session_id: self.session_id,
104 }))
105 } else {
106 None
107 }
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use serde_json::json;
115
116 #[test]
119 fn serialize_command_without_params_or_session() {
120 let cmd = CdpCommand {
121 id: 1,
122 method: "Browser.getVersion".into(),
123 params: None,
124 session_id: None,
125 };
126 let json: Value = serde_json::to_value(&cmd).unwrap();
127 assert_eq!(json["id"], 1);
128 assert_eq!(json["method"], "Browser.getVersion");
129 assert!(json.get("params").is_none());
130 assert!(json.get("sessionId").is_none());
131 }
132
133 #[test]
134 fn serialize_command_with_params() {
135 let cmd = CdpCommand {
136 id: 2,
137 method: "Page.navigate".into(),
138 params: Some(json!({"url": "https://example.com"})),
139 session_id: None,
140 };
141 let json: Value = serde_json::to_value(&cmd).unwrap();
142 assert_eq!(json["id"], 2);
143 assert_eq!(json["params"]["url"], "https://example.com");
144 assert!(json.get("sessionId").is_none());
145 }
146
147 #[test]
148 fn serialize_command_with_session_id() {
149 let cmd = CdpCommand {
150 id: 3,
151 method: "Runtime.evaluate".into(),
152 params: Some(json!({"expression": "1+1"})),
153 session_id: Some("session-abc".into()),
154 };
155 let json: Value = serde_json::to_value(&cmd).unwrap();
156 assert_eq!(json["sessionId"], "session-abc");
157 }
158
159 #[test]
162 fn deserialize_success_response() {
163 let raw: RawCdpMessage =
164 serde_json::from_str(r#"{"id": 1, "result": {"frameId": "abc"}}"#).unwrap();
165 assert_eq!(raw.id, Some(1));
166 assert!(raw.result.is_some());
167 assert!(raw.error.is_none());
168 assert!(raw.method.is_none());
169 }
170
171 #[test]
172 fn deserialize_error_response() {
173 let raw: RawCdpMessage =
174 serde_json::from_str(r#"{"id": 2, "error": {"code": -32000, "message": "Not found"}}"#)
175 .unwrap();
176 assert_eq!(raw.id, Some(2));
177 assert!(raw.error.is_some());
178 let err = raw.error.unwrap();
179 assert_eq!(err.code, -32000);
180 assert_eq!(err.message, "Not found");
181 }
182
183 #[test]
184 fn deserialize_event() {
185 let raw: RawCdpMessage = serde_json::from_str(
186 r#"{"method": "Page.loadEventFired", "params": {"timestamp": 123.456}}"#,
187 )
188 .unwrap();
189 assert!(raw.id.is_none());
190 assert_eq!(raw.method.as_deref(), Some("Page.loadEventFired"));
191 assert!(raw.params.is_some());
192 }
193
194 #[test]
195 fn deserialize_session_scoped_event() {
196 let raw: RawCdpMessage = serde_json::from_str(
197 r#"{"method": "DOM.documentUpdated", "params": {}, "sessionId": "sess-1"}"#,
198 )
199 .unwrap();
200 assert_eq!(raw.session_id.as_deref(), Some("sess-1"));
201 }
202
203 #[test]
204 fn deserialize_session_scoped_response() {
205 let raw: RawCdpMessage =
206 serde_json::from_str(r#"{"id": 5, "result": {}, "sessionId": "sess-2"}"#).unwrap();
207 assert_eq!(raw.id, Some(5));
208 assert_eq!(raw.session_id.as_deref(), Some("sess-2"));
209 }
210
211 #[test]
214 fn classify_response() {
215 let raw: RawCdpMessage =
216 serde_json::from_str(r#"{"id": 1, "result": {"ok": true}}"#).unwrap();
217 let kind = raw.classify();
218 assert!(matches!(kind, Some(MessageKind::Response(_))));
219 if let Some(MessageKind::Response(resp)) = kind {
220 assert_eq!(resp.id, 1);
221 assert!(resp.result.is_ok());
222 }
223 }
224
225 #[test]
226 fn classify_error_response() {
227 let raw: RawCdpMessage = serde_json::from_str(
228 r#"{"id": 2, "error": {"code": -32600, "message": "Invalid request"}}"#,
229 )
230 .unwrap();
231 let kind = raw.classify();
232 assert!(matches!(kind, Some(MessageKind::Response(_))));
233 if let Some(MessageKind::Response(resp)) = kind {
234 assert_eq!(resp.id, 2);
235 assert!(resp.result.is_err());
236 let err = resp.result.unwrap_err();
237 assert_eq!(err.code, -32600);
238 }
239 }
240
241 #[test]
242 fn classify_event() {
243 let raw: RawCdpMessage = serde_json::from_str(
244 r#"{"method": "Network.requestWillBeSent", "params": {"requestId": "r1"}}"#,
245 )
246 .unwrap();
247 let kind = raw.classify();
248 assert!(matches!(kind, Some(MessageKind::Event(_))));
249 if let Some(MessageKind::Event(event)) = kind {
250 assert_eq!(event.method, "Network.requestWillBeSent");
251 assert_eq!(event.params["requestId"], "r1");
252 }
253 }
254
255 #[test]
256 fn classify_unclassifiable_returns_none() {
257 let raw: RawCdpMessage = serde_json::from_str(r"{}").unwrap();
258 assert!(raw.classify().is_none());
259 }
260
261 #[test]
262 fn classify_response_without_result_yields_null() {
263 let raw: RawCdpMessage = serde_json::from_str(r#"{"id": 10}"#).unwrap();
264 if let Some(MessageKind::Response(resp)) = raw.classify() {
265 assert_eq!(resp.result.unwrap(), Value::Null);
266 } else {
267 panic!("expected response");
268 }
269 }
270
271 #[test]
272 fn classify_event_without_params_yields_null() {
273 let raw: RawCdpMessage =
274 serde_json::from_str(r#"{"method": "Page.frameNavigated"}"#).unwrap();
275 if let Some(MessageKind::Event(event)) = raw.classify() {
276 assert_eq!(event.params, Value::Null);
277 } else {
278 panic!("expected event");
279 }
280 }
281
282 #[test]
285 fn message_ids_are_unique_and_monotonic() {
286 use std::sync::atomic::{AtomicU64, Ordering};
287 let counter = AtomicU64::new(1);
289 let id1 = counter.fetch_add(1, Ordering::Relaxed);
290 let id2 = counter.fetch_add(1, Ordering::Relaxed);
291 let id3 = counter.fetch_add(1, Ordering::Relaxed);
292 assert!(id2 > id1);
293 assert!(id3 > id2);
294 }
295}