Skip to main content

chrome_cli/cdp/
types.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4/// Outgoing CDP command (client to Chrome).
5#[derive(Debug, Serialize)]
6pub struct CdpCommand {
7    /// Unique message ID for response correlation.
8    pub id: u64,
9    /// CDP method name (e.g., `Page.navigate`).
10    pub method: String,
11    /// Optional parameters for the command.
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub params: Option<Value>,
14    /// Optional session ID for session-scoped commands.
15    #[serde(rename = "sessionId", skip_serializing_if = "Option::is_none")]
16    pub session_id: Option<String>,
17}
18
19/// Raw incoming CDP message before classification.
20///
21/// This is the union of response and event fields — every incoming
22/// WebSocket message is deserialized into this type first, then
23/// classified via [`classify`](Self::classify).
24#[derive(Debug, Deserialize)]
25pub struct RawCdpMessage {
26    /// Present for responses; absent for events.
27    pub id: Option<u64>,
28    /// Present for events (and some responses with `method`).
29    pub method: Option<String>,
30    /// Event parameters or additional response data.
31    pub params: Option<Value>,
32    /// Successful response payload.
33    pub result: Option<Value>,
34    /// Protocol error payload.
35    pub error: Option<CdpProtocolError>,
36    /// Session ID for session-scoped messages.
37    #[serde(rename = "sessionId")]
38    pub session_id: Option<String>,
39}
40
41/// CDP protocol error payload returned by Chrome.
42#[derive(Debug, Clone, Deserialize)]
43pub struct CdpProtocolError {
44    /// The CDP error code (e.g., -32000).
45    pub code: i64,
46    /// Human-readable error description.
47    pub message: String,
48}
49
50/// Parsed CDP response (has an `id`).
51#[derive(Debug)]
52pub struct CdpResponse {
53    /// The message ID that correlates to the sent command.
54    pub id: u64,
55    /// The result: either a successful value or a protocol error.
56    pub result: Result<Value, CdpProtocolError>,
57    /// Session ID if this response is session-scoped.
58    pub session_id: Option<String>,
59}
60
61/// Parsed CDP event (no `id`, has `method`).
62#[derive(Debug, Clone)]
63pub struct CdpEvent {
64    /// The CDP event method name (e.g., `Page.loadEventFired`).
65    pub method: String,
66    /// Event parameters.
67    pub params: Value,
68    /// Session ID if this event is session-scoped.
69    pub session_id: Option<String>,
70}
71
72/// Classification of a raw CDP message.
73pub enum MessageKind {
74    /// A response to a previously sent command.
75    Response(CdpResponse),
76    /// An asynchronous event from Chrome.
77    Event(CdpEvent),
78}
79
80impl RawCdpMessage {
81    /// Classify this raw message as either a response or an event.
82    ///
83    /// Messages with an `id` field are responses; messages with a `method`
84    /// field but no `id` are events. Returns `None` if the message cannot
85    /// be classified (neither `id` nor `method` present).
86    #[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    // --- CdpCommand serialization ---
117
118    #[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    // --- RawCdpMessage deserialization ---
160
161    #[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    // --- classify() ---
212
213    #[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    // --- Message ID ---
283
284    #[test]
285    fn message_ids_are_unique_and_monotonic() {
286        use std::sync::atomic::{AtomicU64, Ordering};
287        // Mirrors the pattern used by TransportHandle::next_message_id
288        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}