headless_chrome/
types.rs

1use crate::protocol::cdp::{
2    types::{Event, JsUInt},
3    Browser,
4    Network::{CookieParam, DeleteCookies},
5    Page,
6    Page::PrintToPDF,
7    DOM::Node,
8};
9
10use serde::{Deserialize, Serialize};
11
12use serde_json::Value;
13
14pub type CallId = JsUInt;
15
16use thiserror::Error;
17
18use anyhow::Result;
19
20type JsInt = i32;
21
22#[derive(Deserialize, Debug, PartialEq, Clone, Error)]
23#[error("Method call error {}: {}", code, message)]
24pub struct RemoteError {
25    pub code: JsInt,
26    pub message: String,
27}
28
29#[derive(Deserialize, Debug, PartialEq, Clone)]
30pub struct Response {
31    #[serde(rename(deserialize = "id"))]
32    pub call_id: CallId,
33    pub result: Option<Value>,
34    pub error: Option<RemoteError>,
35}
36
37pub fn parse_response<T>(response: Response) -> Result<T>
38where
39    T: serde::de::DeserializeOwned + std::fmt::Debug,
40{
41    if let Some(error) = response.error {
42        return Err(error.into());
43    }
44
45    let result: T = serde_json::from_value(response.result.unwrap())?;
46
47    Ok(result)
48}
49
50#[derive(Deserialize, Debug, Clone)]
51#[serde(untagged)]
52#[allow(clippy::large_enum_variant)]
53pub enum Message {
54    Event(Event),
55    Response(Response),
56    ConnectionShutdown,
57}
58
59#[derive(Deserialize, Serialize, Debug)]
60pub struct TransferMode {
61    mode: String,
62}
63
64impl From<TransferMode> for Option<Page::PrintToPDFTransfer_modeOption> {
65    fn from(val: TransferMode) -> Self {
66        if val.mode == "base64" {
67            Some(Page::PrintToPDFTransfer_modeOption::ReturnAsBase64)
68        } else if val.mode == "stream" {
69            Some(Page::PrintToPDFTransfer_modeOption::ReturnAsStream)
70        } else {
71            None
72        }
73    }
74}
75
76#[derive(Deserialize, Serialize, Debug, Default)]
77#[serde(rename_all = "camelCase")]
78pub struct PrintToPdfOptions {
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub landscape: Option<bool>,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub display_header_footer: Option<bool>,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub print_background: Option<bool>,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub scale: Option<f64>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub paper_width: Option<f64>,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub paper_height: Option<f64>,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub margin_top: Option<f64>,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub margin_bottom: Option<f64>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub margin_left: Option<f64>,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub margin_right: Option<f64>,
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub page_ranges: Option<String>,
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub ignore_invalid_page_ranges: Option<bool>,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub header_template: Option<String>,
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub footer_template: Option<String>,
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub prefer_css_page_size: Option<bool>,
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub transfer_mode: Option<TransferMode>,
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub generate_document_outline: Option<bool>,
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub generate_tagged_pdf: Option<bool>,
115}
116
117pub fn parse_raw_message(raw_message: &str) -> Result<Message> {
118    Ok(serde_json::from_str::<Message>(raw_message)?)
119}
120
121#[derive(Clone, Debug)]
122pub enum Bounds {
123    Minimized,
124    Maximized,
125    Fullscreen,
126    Normal {
127        /// The offset from the left edge of the screen to the window in pixels.
128        left: Option<JsUInt>,
129        /// The offset from the top edge of the screen to the window in pixels.
130        top: Option<JsUInt>,
131        /// The window width in pixels.
132        width: Option<f64>,
133        /// THe window height in pixels.
134        height: Option<f64>,
135    },
136}
137
138impl Bounds {
139    /// Set normal window state without setting any coordinates
140    pub fn normal() -> Self {
141        Self::Normal {
142            left: None,
143            top: None,
144            width: None,
145            height: None,
146        }
147    }
148}
149
150impl From<CookieParam> for DeleteCookies {
151    fn from(v: CookieParam) -> Self {
152        Self {
153            name: v.name,
154            url: v.url,
155            domain: v.domain,
156            path: v.path,
157            partition_key: v.partition_key,
158        }
159    }
160}
161
162impl From<Bounds> for Browser::Bounds {
163    fn from(val: Bounds) -> Self {
164        match val {
165            Bounds::Minimized => Browser::Bounds {
166                left: None,
167                top: None,
168                width: None,
169                height: None,
170                window_state: Some(Browser::WindowState::Minimized),
171            },
172            Bounds::Maximized => Browser::Bounds {
173                left: None,
174                top: None,
175                width: None,
176                height: None,
177                window_state: Some(Browser::WindowState::Maximized),
178            },
179            Bounds::Fullscreen => Browser::Bounds {
180                left: None,
181                top: None,
182                width: None,
183                height: None,
184                window_state: Some(Browser::WindowState::Fullscreen),
185            },
186            Bounds::Normal {
187                left,
188                top,
189                width,
190                height,
191            } => Browser::Bounds {
192                left,
193                top,
194                width: width.map(|f| f as u32),
195                height: height.map(|f| f as u32),
196                window_state: Some(Browser::WindowState::Normal),
197            },
198        }
199    }
200}
201
202#[derive(Clone, Debug)]
203pub struct CurrentBounds {
204    pub left: JsUInt,
205    pub top: JsUInt,
206    pub width: f64,
207    pub height: f64,
208    pub state: Browser::WindowState,
209}
210
211impl From<Browser::Bounds> for CurrentBounds {
212    fn from(bounds: Browser::Bounds) -> Self {
213        Self {
214            left: bounds.left.unwrap(),
215            top: bounds.top.unwrap(),
216            width: f64::from(bounds.width.unwrap()),
217            height: f64::from(bounds.height.unwrap()),
218            state: bounds.window_state.unwrap(),
219        }
220    }
221}
222
223impl Default for PrintToPDF {
224    fn default() -> Self {
225        PrintToPDF {
226            display_header_footer: None,
227            footer_template: None,
228            generate_document_outline: None,
229            generate_tagged_pdf: None,
230            header_template: None,
231            landscape: None,
232            margin_bottom: None,
233            margin_left: None,
234            margin_right: None,
235            margin_top: None,
236            page_ranges: None,
237            paper_height: None,
238            paper_width: None,
239            prefer_css_page_size: None,
240            print_background: None,
241            scale: None,
242            transfer_mode: None,
243        }
244    }
245}
246
247struct SearchVisitor<'a, F> {
248    predicate: F,
249    item: Option<&'a Node>,
250}
251
252impl<'a, F: FnMut(&Node) -> bool> SearchVisitor<'a, F> {
253    fn new(predicate: F) -> Self {
254        SearchVisitor {
255            predicate,
256            item: None,
257        }
258    }
259
260    fn visit(&mut self, n: &'a Node) {
261        if (self.predicate)(n) {
262            self.item = Some(n);
263        } else if self.item.is_none() {
264            if let Some(c) = &n.children {
265                c.iter().for_each(|n| self.visit(n));
266            }
267        }
268    }
269}
270
271impl Node {
272    /// Returns the first node for which the given closure returns true.
273    ///
274    /// Nodes are inspected breadth-first down their children.
275    pub fn find<F: FnMut(&Self) -> bool>(&self, predicate: F) -> Option<&Self> {
276        let mut s = SearchVisitor::new(predicate);
277        s.visit(self);
278        s.item
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use log::trace;
285    use serde_json::json;
286
287    use super::*;
288
289    #[test]
290    fn pass_through_channel() {
291        env_logger::try_init().unwrap_or(());
292
293        let attached_to_target_json = json!({
294            "method": "Target.attachedToTarget",
295            "params": {
296                "sessionId": "8BEF122ABAB0C43B5729585A537F424A",
297                "targetInfo": {
298                    "targetId": "26DEBCB2A45BEFC67A84012AC32C8B2A",
299                    "type": "page",
300                    "title": "",
301                    "url": "about:blank",
302                    "attached": true,
303                    "browserContextId": "946423F3D201EFA1A5FCF3462E340C15"
304                },
305                "waitingForDebugger": false
306            }
307        });
308
309        let _event: Message = serde_json::from_value(attached_to_target_json).unwrap();
310    }
311
312    #[test]
313    fn parse_event_fully() {
314        env_logger::try_init().unwrap_or(());
315
316        let attached_to_target_json = json!({
317            "method": "Target.attachedToTarget",
318            "params": {
319                "sessionId": "8BEF122ABAB0C43B5729585A537F424A",
320                "targetInfo": {
321                    "targetId": "26DEBCB2A45BEFC67A84012AC32C8B2A",
322                    "type": "page",
323                    "title": "",
324                    "url": "about:blank",
325                    "attached": true,
326                    "browserContextId": "946423F3D201EFA1A5FCF3462E340C15"
327                },
328                "waitingForDebugger": false
329            }
330        });
331
332        if let Ok(Event::AttachedToTarget(_)) = serde_json::from_value(attached_to_target_json) {
333        } else {
334            panic!("Failed to parse event properly");
335        }
336
337        let received_target_msg_event = json!({
338            "method": "Target.receivedMessageFromTarget",
339            "params": {
340                "sessionId": "8BEF122ABAB0C43B5729585A537F424A",
341                "message": "{\"id\":43473,\"result\":{\"data\":\"kDEgAABII=\"}}",
342                "targetId": "26DEBCB2A45BEFC67A84012AC32C8B2A"
343            }
344        });
345        let event: Event = serde_json::from_value(received_target_msg_event).unwrap();
346        match event {
347            Event::ReceivedMessageFromTarget(ev) => {
348                trace!("{:?}", ev);
349            }
350            _ => panic!("bad news"),
351        }
352    }
353
354    #[test]
355    fn easy_parse_messages() {
356        env_logger::try_init().unwrap_or(());
357
358        let example_message_strings = [
359            // browser method response:
360            "{\"id\":1,\"result\":{\"browserContextIds\":[\"C2652EACAAA12B41038F1F2137C57A6E\"]}}",
361            "{\"id\":2,\"result\":{\"targetInfos\":[{\"targetId\":\"225A1B90036320AB4DB2E28F04AA6EE0\",\"type\":\"page\",\"title\":\"\",\"url\":\"about:blank\",\"attached\":false,\"browserContextId\":\"04FB807A65CFCA420C03E1134EB9214E\"}]}}",
362            "{\"id\":3,\"result\":{}}",
363            // browser event:
364            "{\"method\":\"Target.attachedToTarget\",\"params\":{\"sessionId\":\"8BEF122ABAB0C43B5729585A537F424A\",\"targetInfo\":{\"targetId\":\"26DEBCB2A45BEFC67A84012AC32C8B2A\",\"type\":\"page\",\"title\":\"\",\"url\":\"about:blank\",\"attached\":true,\"browserContextId\":\"946423F3D201EFA1A5FCF3462E340C15\"},\"waitingForDebugger\":false}}",
365            // browser event which indicates target method response:
366            "{\"method\":\"Target.receivedMessageFromTarget\",\"params\":{\"sessionId\":\"8BEF122ABAB0C43B5729585A537F424A\",\"message\":\"{\\\"id\\\":43473,\\\"result\\\":{\\\"data\\\":\\\"iVBORw0KGgoAAAANSUhEUgAAAyAAAAJYCAYAAACadoJwAAAMa0lEQVR4nO3XMQEAIAzAMMC/5+GiHCQK+nbPzCwAAIDAeR0AAAD8w4AAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABII=\\\"}}\",\"targetId\":\"26DEBCB2A45BEFC67A84012AC32C8B2A\"}}"
367        ];
368
369        for msg_string in &example_message_strings {
370            let _message: super::Message = parse_raw_message(msg_string).unwrap();
371        }
372    }
373}