headless_chrome 0.9.0

Control Chrome programatically
Documentation
//! For (de)serializing method calls and events from the Chrome DevTools Protocol.

use std::fmt::Debug;

use crate::protocol::types::{JsInt, JsUInt};
use failure::{Fail, Fallible};
use serde;
use serde::{Deserialize, Serialize};
use serde_json::Value;

pub mod browser;
pub mod debugger;
pub mod dom;
pub mod input;
pub mod logs;
pub mod network;
pub mod page;
pub mod profiler;
pub mod runtime;
pub mod target;
pub mod types;

/// Browser window Id
type WindowId = JsUInt;

pub type CallId = JsUInt;

#[derive(Serialize, Debug)]
pub struct MethodCall<T>
where
    T: Debug,
{
    #[serde(rename = "method")]
    method_name: &'static str,
    pub id: CallId,
    params: T,
}

impl<T> MethodCall<T>
where
    T: Debug,
{
    pub fn get_params(&self) -> &T {
        &self.params
    }
}

pub trait Method: Debug {
    const NAME: &'static str;

    type ReturnObject: serde::de::DeserializeOwned + std::fmt::Debug; // have this = something?

    // TODO: Rust IntelliJ says that to_* method calls "usuall take self by reference"
    //       Maybe that'd be better here?
    fn to_method_call(self, call_id: CallId) -> MethodCall<Self>
    where
        Self: std::marker::Sized,
    {
        MethodCall {
            id: call_id,
            params: self,
            method_name: Self::NAME,
        }
    }
}

#[derive(Deserialize, Debug, PartialEq, Clone, Fail)]
#[fail(display = "Method call error {}: {}", code, message)]
pub struct RemoteError {
    pub code: JsInt,
    pub message: String,
}

#[derive(Deserialize, Debug, PartialEq, Clone)]
pub struct Response {
    #[serde(rename(deserialize = "id"))]
    pub call_id: CallId,
    pub result: Option<Value>,
    pub error: Option<RemoteError>,
}

pub fn parse_response<T>(response: Response) -> Fallible<T>
where
    T: serde::de::DeserializeOwned + std::fmt::Debug,
{
    if let Some(error) = response.error {
        return Err(error.into());
    }

    let result: T = serde_json::from_value(response.result.unwrap()).unwrap();

    Ok(result)
}

#[derive(Deserialize, Debug, Clone, PartialEq)]
#[serde(tag = "method")]
#[allow(clippy::large_enum_variant)]
pub enum Event {
    #[serde(rename = "Target.attachedToTarget")]
    AttachedToTarget(target::events::AttachedToTargetEvent),
    #[serde(rename = "Target.receivedMessageFromTarget")]
    ReceivedMessageFromTarget(target::events::ReceivedMessageFromTargetEvent),
    #[serde(rename = "Target.targetInfoChanged")]
    TargetInfoChanged(target::events::TargetInfoChangedEvent),
    #[serde(rename = "Target.targetCreated")]
    TargetCreated(target::events::TargetCreatedEvent),
    #[serde(rename = "Target.targetDestroyed")]
    TargetDestroyed(target::events::TargetDestroyedEvent),
    #[serde(rename = "Page.frameStartedLoading")]
    FrameStartedLoading(page::events::FrameStartedLoadingEvent),
    #[serde(rename = "Page.frameNavigated")]
    FrameNavigated(page::events::FrameNavigatedEvent),
    #[serde(rename = "Page.frameStoppedLoading")]
    FrameStoppedLoading(page::events::FrameStoppedLoadingEvent),
    #[serde(rename = "Page.lifecycleEvent")]
    Lifecycle(page::events::LifecycleEvent),
    #[serde(rename = "Network.requestIntercepted")]
    RequestIntercepted(network::events::RequestInterceptedEvent),
    #[serde(rename = "Network.responseReceived")]
    ResponseReceived(network::events::ResponseReceivedEvent),
    #[serde(rename = "Log.entryAdded")]
    LogEntryAdded(logs::events::EntryAddedEvent),
    #[serde(rename = "Runtime.exceptionThrown")]
    RuntimeExceptionThrown(runtime::events::ExceptionThrownEvent),
}

#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum Message {
    Event(Event),
    Response(Response),
    ConnectionShutdown,
}

#[cfg(test)]
mod tests {
    use log::*;
    use serde_json::json;

    use super::*;

    #[test]
    fn pass_through_channel() {
        env_logger::try_init().unwrap_or(());

        let attached_to_target_json = json!({
            "method": "Target.attachedToTarget",
            "params": {
                "sessionId": "8BEF122ABAB0C43B5729585A537F424A",
                "targetInfo": {
                    "targetId": "26DEBCB2A45BEFC67A84012AC32C8B2A",
                    "type": "page",
                    "title": "",
                    "url": "about:blank",
                    "attached": true,
                    "browserContextId": "946423F3D201EFA1A5FCF3462E340C15"
                },
                "waitingForDebugger": false
            }
        });

        let _event: Message = serde_json::from_value(attached_to_target_json).unwrap();
    }

    #[test]
    fn parse_event_fully() {
        env_logger::try_init().unwrap_or(());

        let attached_to_target_json = json!({
            "method": "Target.attachedToTarget",
            "params": {
                "sessionId": "8BEF122ABAB0C43B5729585A537F424A",
                "targetInfo": {
                    "targetId": "26DEBCB2A45BEFC67A84012AC32C8B2A",
                    "type": "page",
                    "title": "",
                    "url": "about:blank",
                    "attached": true,
                    "browserContextId": "946423F3D201EFA1A5FCF3462E340C15"
                },
                "waitingForDebugger": false
            }
        });

        if let Ok(Event::AttachedToTarget(_)) = serde_json::from_value(attached_to_target_json) {
        } else {
            panic!("Failed to parse event properly");
        }

        let received_target_msg_event = json!({
            "method": "Target.receivedMessageFromTarget",
            "params": {
                "sessionId": "8BEF122ABAB0C43B5729585A537F424A",
                "message": "{\"id\":43473,\"result\":{\"data\":\"kDEgAABII=\"}}",
                "targetId": "26DEBCB2A45BEFC67A84012AC32C8B2A"
            }
        });
        let event: Event = serde_json::from_value(received_target_msg_event).unwrap();
        match event {
            Event::ReceivedMessageFromTarget(ev) => {
                trace!("{:?}", ev);
            }
            _ => panic!("bad news"),
        }
    }

    #[test]
    fn easy_parse_messages() {
        env_logger::try_init().unwrap_or(());

        let example_message_strings = [
            // browser method response:
            "{\"id\":1,\"result\":{\"browserContextIds\":[\"C2652EACAAA12B41038F1F2137C57A6E\"]}}",
            "{\"id\":2,\"result\":{\"targetInfos\":[{\"targetId\":\"225A1B90036320AB4DB2E28F04AA6EE0\",\"type\":\"page\",\"title\":\"\",\"url\":\"about:blank\",\"attached\":false,\"browserContextId\":\"04FB807A65CFCA420C03E1134EB9214E\"}]}}",
            "{\"id\":3,\"result\":{}}",
            // browser event:
            "{\"method\":\"Target.attachedToTarget\",\"params\":{\"sessionId\":\"8BEF122ABAB0C43B5729585A537F424A\",\"targetInfo\":{\"targetId\":\"26DEBCB2A45BEFC67A84012AC32C8B2A\",\"type\":\"page\",\"title\":\"\",\"url\":\"about:blank\",\"attached\":true,\"browserContextId\":\"946423F3D201EFA1A5FCF3462E340C15\"},\"waitingForDebugger\":false}}",
            // browser event which indicates target method response:
            "{\"method\":\"Target.receivedMessageFromTarget\",\"params\":{\"sessionId\":\"8BEF122ABAB0C43B5729585A537F424A\",\"message\":\"{\\\"id\\\":43473,\\\"result\\\":{\\\"data\\\":\\\"iVBORw0KGgoAAAANSUhEUgAAAyAAAAJYCAYAAACadoJwAAAMa0lEQVR4nO3XMQEAIAzAMMC/5+GiHCQK+nbPzCwAAIDAeR0AAAD8w4AAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABAxoAAAAAZAwIAAGQMCAAAkDEgAABII=\\\"}}\",\"targetId\":\"26DEBCB2A45BEFC67A84012AC32C8B2A\"}}"
        ];

        for msg_string in &example_message_strings {
            let _message: super::Message = parse_raw_message(msg_string).unwrap();
        }
    }
}

pub fn parse_raw_message(raw_message: &str) -> Fallible<Message> {
    Ok(serde_json::from_str::<Message>(raw_message)?)
}