Skip to main content

firefox_webdriver/protocol/
request.rs

1//! Request and Response message types.
2//!
3//! Defines the message format for command requests and responses
4//! between local end (Rust) and remote end (Extension).
5//!
6//! See ARCHITECTURE.md Section 2.2-2.3 for specification.
7
8// ============================================================================
9// Imports
10// ============================================================================
11
12use serde::ser::{SerializeMap, Serializer};
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16use crate::error::{Error, Result};
17use crate::identifiers::{FrameId, RequestId, TabId};
18
19use super::Command;
20
21// ============================================================================
22// Request
23// ============================================================================
24
25/// A command request from local end to remote end.
26///
27/// # Format
28///
29/// ```json
30/// {
31///   "id": "uuid",
32///   "method": "module.methodName",
33///   "tabId": 1,
34///   "frameId": 0,
35///   "params": { ... }
36/// }
37/// ```
38#[derive(Debug, Clone)]
39pub struct Request {
40    /// Unique identifier for request/response correlation.
41    pub id: RequestId,
42
43    /// Target tab ID.
44    pub tab_id: TabId,
45
46    /// Target frame ID (0 = main frame).
47    pub frame_id: FrameId,
48
49    /// Command with method and params.
50    pub command: Command,
51}
52
53impl Serialize for Request {
54    fn serialize<S: Serializer>(
55        &self,
56        serializer: S,
57    ) -> std::result::Result<S::Ok, S::Error> {
58        // The command already serializes to { "method": "...", "params": { ... } }
59        // We need to add id, tabId, frameId alongside it
60        let command_value =
61            serde_json::to_value(&self.command).map_err(serde::ser::Error::custom)?;
62        let mut map = serializer.serialize_map(None)?;
63        map.serialize_entry("id", &self.id)?;
64        map.serialize_entry("tabId", &self.tab_id)?;
65        map.serialize_entry("frameId", &self.frame_id)?;
66        // Merge command fields
67        if let serde_json::Value::Object(obj) = command_value {
68            for (k, v) in obj {
69                map.serialize_entry(&k, &v)?;
70            }
71        }
72        map.end()
73    }
74}
75
76impl Request {
77    /// Creates a new request with auto-generated ID.
78    #[inline]
79    #[must_use]
80    pub fn new(tab_id: TabId, frame_id: FrameId, command: Command) -> Self {
81        Self {
82            id: RequestId::generate(),
83            tab_id,
84            frame_id,
85            command,
86        }
87    }
88
89    /// Creates a new request with specific ID.
90    #[inline]
91    #[must_use]
92    pub fn with_id(id: RequestId, tab_id: TabId, frame_id: FrameId, command: Command) -> Self {
93        Self {
94            id,
95            tab_id,
96            frame_id,
97            command,
98        }
99    }
100}
101
102// ============================================================================
103// Response
104// ============================================================================
105
106/// A response from remote end to local end.
107///
108/// # Format
109///
110/// Success:
111/// ```json
112/// {
113///   "id": "uuid",
114///   "type": "success",
115///   "result": { ... }
116/// }
117/// ```
118///
119/// Error:
120/// ```json
121/// {
122///   "id": "uuid",
123///   "type": "error",
124///   "error": "error code",
125///   "message": "error message"
126/// }
127/// ```
128#[derive(Debug, Clone, Deserialize)]
129pub struct Response {
130    /// Matches the command `id`.
131    pub id: RequestId,
132
133    /// Response type.
134    #[serde(rename = "type")]
135    pub response_type: ResponseType,
136
137    /// Result data (if success).
138    #[serde(default)]
139    pub result: Option<Value>,
140
141    /// Error code (if error).
142    #[serde(default)]
143    pub error: Option<String>,
144
145    /// Error message (if error).
146    #[serde(default)]
147    pub message: Option<String>,
148}
149
150impl Response {
151    /// Returns `true` if this is a success response.
152    #[inline]
153    #[must_use]
154    pub fn is_success(&self) -> bool {
155        self.response_type == ResponseType::Success
156    }
157
158    /// Returns `true` if this is an error response.
159    #[inline]
160    #[must_use]
161    pub fn is_error(&self) -> bool {
162        self.response_type == ResponseType::Error
163    }
164
165    /// Extracts the result value, returning error if response was error.
166    ///
167    /// # Errors
168    ///
169    /// Returns [`Error::Protocol`] if the response was an error.
170    pub fn into_result(self) -> Result<Value> {
171        match self.response_type {
172            ResponseType::Success => Ok(self.result.unwrap_or(Value::Null)),
173            ResponseType::Error => {
174                let error_code = self.error.unwrap_or_else(|| "unknown error".to_string());
175                let message = self.message.unwrap_or_else(|| error_code.clone());
176                Err(Error::protocol(message))
177            }
178        }
179    }
180
181    /// Gets a string value from the result as a borrowed reference.
182    ///
183    /// Returns `None` if key not found or not a string.
184    #[inline]
185    #[must_use]
186    pub fn get_str(&self, key: &str) -> Option<&str> {
187        self.result
188            .as_ref()
189            .and_then(|v| v.get(key))
190            .and_then(|v| v.as_str())
191    }
192
193    /// Gets a string value from the result.
194    ///
195    /// Returns empty string if key not found or not a string.
196    #[inline]
197    #[must_use]
198    pub fn get_string(&self, key: &str) -> String {
199        self.get_str(key).unwrap_or_default().to_string()
200    }
201
202    /// Gets a u64 value from the result.
203    ///
204    /// Returns 0 if key not found or not a number.
205    #[inline]
206    #[must_use]
207    pub fn get_u64(&self, key: &str) -> u64 {
208        self.result
209            .as_ref()
210            .and_then(|v| v.get(key))
211            .and_then(|v| v.as_u64())
212            .unwrap_or_default()
213    }
214
215    /// Gets a boolean value from the result.
216    ///
217    /// Returns false if key not found or not a boolean.
218    #[inline]
219    #[must_use]
220    pub fn get_bool(&self, key: &str) -> bool {
221        self.result
222            .as_ref()
223            .and_then(|v| v.get(key))
224            .and_then(|v| v.as_bool())
225            .unwrap_or_default()
226    }
227}
228
229// ============================================================================
230// ResponseType
231// ============================================================================
232
233/// Response type discriminator.
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
235#[serde(rename_all = "lowercase")]
236pub enum ResponseType {
237    /// Successful response.
238    Success,
239    /// Error response.
240    Error,
241}
242
243// ============================================================================
244// Tests
245// ============================================================================
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use crate::protocol::BrowsingContextCommand;
251
252    #[test]
253    fn test_request_serialization() {
254        let tab_id = TabId::new(1).expect("valid tab id");
255        let frame_id = FrameId::main();
256        let command = Command::BrowsingContext(BrowsingContextCommand::Navigate {
257            url: "https://example.com".to_string(),
258        });
259
260        let request = Request::new(tab_id, frame_id, command);
261        let json = serde_json::to_string(&request).expect("serialize");
262
263        assert!(json.contains("browsingContext.navigate"));
264        assert!(json.contains("tabId"));
265        assert!(json.contains("frameId"));
266    }
267
268    #[test]
269    fn test_request_with_id() {
270        let id = RequestId::generate();
271        let tab_id = TabId::new(1).expect("valid tab id");
272        let frame_id = FrameId::main();
273        let command = Command::BrowsingContext(BrowsingContextCommand::GetTitle);
274
275        let request = Request::with_id(id, tab_id, frame_id, command);
276        assert_eq!(request.id, id);
277    }
278
279    #[test]
280    fn test_success_response() {
281        let json_str = r#"{
282            "id": "550e8400-e29b-41d4-a716-446655440000",
283            "type": "success",
284            "result": {"title": "Example"}
285        }"#;
286
287        let response: Response = serde_json::from_str(json_str).expect("parse");
288        assert!(response.is_success());
289        assert!(!response.is_error());
290        assert_eq!(response.get_string("title"), "Example");
291    }
292
293    #[test]
294    fn test_error_response() {
295        let json_str = r#"{
296            "id": "550e8400-e29b-41d4-a716-446655440000",
297            "type": "error",
298            "error": "no such element",
299            "message": "Element not found"
300        }"#;
301
302        let response: Response = serde_json::from_str(json_str).expect("parse");
303        assert!(response.is_error());
304        assert!(!response.is_success());
305        assert_eq!(response.error, Some("no such element".to_string()));
306    }
307
308    #[test]
309    fn test_into_result_success() {
310        let json_str = r#"{
311            "id": "550e8400-e29b-41d4-a716-446655440000",
312            "type": "success",
313            "result": {"value": 42}
314        }"#;
315
316        let response: Response = serde_json::from_str(json_str).expect("parse");
317        let result = response.into_result().expect("should succeed");
318        assert_eq!(result.get("value").and_then(|v| v.as_u64()), Some(42));
319    }
320
321    #[test]
322    fn test_into_result_error() {
323        let json_str = r#"{
324            "id": "550e8400-e29b-41d4-a716-446655440000",
325            "type": "error",
326            "error": "timeout",
327            "message": "Operation timed out"
328        }"#;
329
330        let response: Response = serde_json::from_str(json_str).expect("parse");
331        let result = response.into_result();
332        assert!(result.is_err());
333    }
334
335    #[test]
336    fn test_response_get_helpers() {
337        let json_str = r#"{
338            "id": "550e8400-e29b-41d4-a716-446655440000",
339            "type": "success",
340            "result": {
341                "name": "test",
342                "count": 42,
343                "enabled": true
344            }
345        }"#;
346
347        let response: Response = serde_json::from_str(json_str).expect("parse");
348        assert_eq!(response.get_string("name"), "test");
349        assert_eq!(response.get_u64("count"), 42);
350        assert!(response.get_bool("enabled"));
351
352        // Missing keys return defaults
353        assert_eq!(response.get_string("missing"), "");
354        assert_eq!(response.get_u64("missing"), 0);
355        assert!(!response.get_bool("missing"));
356    }
357}