use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use crate::identifiers::RequestId;
#[derive(Debug, Clone, Deserialize)]
pub struct Event {
pub id: RequestId,
#[serde(rename = "type")]
pub event_type: String,
pub method: String,
pub params: Value,
}
impl Event {
#[inline]
#[must_use]
pub fn module(&self) -> &str {
self.method.split('.').next().unwrap_or_default()
}
#[inline]
#[must_use]
pub fn event_name(&self) -> &str {
self.method.split('.').nth(1).unwrap_or_default()
}
#[must_use]
pub fn parse(&self) -> ParsedEvent {
self.parse_internal()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct EventReply {
pub id: RequestId,
#[serde(rename = "replyTo")]
pub reply_to: String,
pub result: Value,
}
impl EventReply {
#[inline]
#[must_use]
pub fn new(id: RequestId, reply_to: impl Into<String>, result: Value) -> Self {
Self {
id,
reply_to: reply_to.into(),
result,
}
}
#[inline]
#[must_use]
pub fn allow(id: RequestId, reply_to: impl Into<String>) -> Self {
Self::new(id, reply_to, json!({ "action": "allow" }))
}
#[inline]
#[must_use]
pub fn block(id: RequestId, reply_to: impl Into<String>) -> Self {
Self::new(id, reply_to, json!({ "action": "block" }))
}
#[inline]
#[must_use]
pub fn redirect(id: RequestId, reply_to: impl Into<String>, url: impl Into<String>) -> Self {
Self::new(
id,
reply_to,
json!({ "action": "redirect", "url": url.into() }),
)
}
}
#[derive(Debug, Clone)]
pub enum ParsedEvent {
BrowsingContextNavigationStarted {
tab_id: u32,
frame_id: u64,
url: String,
},
BrowsingContextDomContentLoaded {
tab_id: u32,
frame_id: u64,
url: String,
},
BrowsingContextLoad {
tab_id: u32,
frame_id: u64,
url: String,
},
BrowsingContextNavigationFailed {
tab_id: u32,
frame_id: u64,
url: String,
error: String,
},
ElementAdded {
strategy: String,
value: String,
element_id: String,
subscription_id: String,
tab_id: u32,
frame_id: u64,
},
ElementRemoved {
element_id: String,
tab_id: u32,
frame_id: u64,
},
ElementAttributeChanged {
element_id: String,
attribute_name: String,
old_value: Option<String>,
new_value: Option<String>,
tab_id: u32,
frame_id: u64,
},
NetworkBeforeRequestSent {
request_id: String,
url: String,
method: String,
resource_type: String,
},
NetworkResponseStarted {
request_id: String,
url: String,
status: u16,
status_text: String,
},
NetworkResponseCompleted {
request_id: String,
url: String,
status: u16,
},
Unknown {
method: String,
params: Value,
},
}
impl Event {
fn parse_internal(&self) -> ParsedEvent {
match self.method.as_str() {
"browsingContext.navigationStarted" => ParsedEvent::BrowsingContextNavigationStarted {
tab_id: self.get_u32("tabId"),
frame_id: self.get_u64("frameId"),
url: self.get_string("url"),
},
"browsingContext.domContentLoaded" => ParsedEvent::BrowsingContextDomContentLoaded {
tab_id: self.get_u32("tabId"),
frame_id: self.get_u64("frameId"),
url: self.get_string("url"),
},
"browsingContext.load" => ParsedEvent::BrowsingContextLoad {
tab_id: self.get_u32("tabId"),
frame_id: self.get_u64("frameId"),
url: self.get_string("url"),
},
"browsingContext.navigationFailed" => ParsedEvent::BrowsingContextNavigationFailed {
tab_id: self.get_u32("tabId"),
frame_id: self.get_u64("frameId"),
url: self.get_string("url"),
error: self.get_string("error"),
},
"element.added" => ParsedEvent::ElementAdded {
strategy: self.get_string("strategy"),
value: self.get_string("value"),
element_id: self.get_string("elementId"),
subscription_id: self.get_string("subscriptionId"),
tab_id: self.get_u32("tabId"),
frame_id: self.get_u64("frameId"),
},
"element.removed" => ParsedEvent::ElementRemoved {
element_id: self.get_string("elementId"),
tab_id: self.get_u32("tabId"),
frame_id: self.get_u64("frameId"),
},
"element.attributeChanged" => ParsedEvent::ElementAttributeChanged {
element_id: self.get_string("elementId"),
attribute_name: self.get_string("attributeName"),
old_value: self.get_optional_string("oldValue"),
new_value: self.get_optional_string("newValue"),
tab_id: self.get_u32("tabId"),
frame_id: self.get_u64("frameId"),
},
"network.beforeRequestSent" => ParsedEvent::NetworkBeforeRequestSent {
request_id: self.get_string("requestId"),
url: self.get_string("url"),
method: self.get_string_or("method", "GET"),
resource_type: self.get_string_or("resourceType", "other"),
},
"network.responseStarted" => ParsedEvent::NetworkResponseStarted {
request_id: self.get_string("requestId"),
url: self.get_string("url"),
status: self.get_u16("status"),
status_text: self.get_string("statusText"),
},
"network.responseCompleted" => ParsedEvent::NetworkResponseCompleted {
request_id: self.get_string("requestId"),
url: self.get_string("url"),
status: self.get_u16("status"),
},
_ => ParsedEvent::Unknown {
method: self.method.clone(),
params: self.params.clone(),
},
}
}
#[inline]
fn get_string(&self, key: &str) -> String {
self.params
.get(key)
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string()
}
#[inline]
fn get_string_or(&self, key: &str, default: &str) -> String {
self.params
.get(key)
.and_then(|v| v.as_str())
.unwrap_or(default)
.to_string()
}
#[inline]
fn get_optional_string(&self, key: &str) -> Option<String> {
self.params
.get(key)
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
#[inline]
fn get_u32(&self, key: &str) -> u32 {
self.params
.get(key)
.and_then(|v| v.as_u64())
.unwrap_or_default() as u32
}
#[inline]
fn get_u64(&self, key: &str) -> u64 {
self.params
.get(key)
.and_then(|v| v.as_u64())
.unwrap_or_default()
}
#[inline]
fn get_u16(&self, key: &str) -> u16 {
self.params
.get(key)
.and_then(|v| v.as_u64())
.unwrap_or_default() as u16
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_event_parsing() {
let json_str = r#"{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "event",
"method": "browsingContext.load",
"params": {
"tabId": 1,
"frameId": 0,
"url": "https://example.com"
}
}"#;
let event: Event = serde_json::from_str(json_str).expect("parse event");
assert_eq!(event.module(), "browsingContext");
assert_eq!(event.event_name(), "load");
let parsed = event.parse();
match parsed {
ParsedEvent::BrowsingContextLoad {
tab_id,
frame_id,
url,
} => {
assert_eq!(tab_id, 1);
assert_eq!(frame_id, 0);
assert_eq!(url, "https://example.com");
}
_ => panic!("unexpected parsed event type"),
}
}
#[test]
fn test_event_reply_allow() {
let id = RequestId::generate();
let reply = EventReply::allow(id, "network.beforeRequestSent");
let json = serde_json::to_string(&reply).expect("serialize");
assert!(json.contains("replyTo"));
assert!(json.contains("allow"));
}
#[test]
fn test_event_reply_block() {
let id = RequestId::generate();
let reply = EventReply::block(id, "network.beforeRequestSent");
let json = serde_json::to_string(&reply).expect("serialize");
assert!(json.contains("block"));
}
#[test]
fn test_event_reply_redirect() {
let id = RequestId::generate();
let reply = EventReply::redirect(id, "network.beforeRequestSent", "https://other.com");
let json = serde_json::to_string(&reply).expect("serialize");
assert!(json.contains("redirect"));
assert!(json.contains("https://other.com"));
}
#[test]
fn test_element_added_parsing() {
let json_str = r##"{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "event",
"method": "element.added",
"params": {
"strategy": "css",
"value": "#login-form",
"elementId": "elem-123",
"subscriptionId": "sub-456",
"tabId": 1,
"frameId": 0
}
}"##;
let event: Event = serde_json::from_str(json_str).expect("parse event");
let parsed = event.parse();
match parsed {
ParsedEvent::ElementAdded {
strategy,
value,
element_id,
..
} => {
assert_eq!(strategy, "css");
assert_eq!(value, "#login-form");
assert_eq!(element_id, "elem-123");
}
_ => panic!("unexpected parsed event type"),
}
}
#[test]
fn test_unknown_event() {
let json_str = r#"{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "event",
"method": "custom.unknownEvent",
"params": { "foo": "bar" }
}"#;
let event: Event = serde_json::from_str(json_str).expect("parse event");
let parsed = event.parse();
match parsed {
ParsedEvent::Unknown { method, .. } => {
assert_eq!(method, "custom.unknownEvent");
}
_ => panic!("expected Unknown variant"),
}
}
}