1use serde::{Deserialize, Serialize};
21use serde_json::{Value, json};
22
23use crate::identifiers::RequestId;
24
25#[derive(Debug, Clone, Deserialize)]
42pub struct Event {
43 pub id: RequestId,
45
46 #[serde(rename = "type")]
48 pub event_type: String,
49
50 pub method: String,
52
53 pub params: Value,
55}
56
57impl Event {
58 #[inline]
67 #[must_use]
68 pub fn module(&self) -> &str {
69 self.method.split('.').next().unwrap_or_default()
70 }
71
72 #[inline]
81 #[must_use]
82 pub fn event_name(&self) -> &str {
83 self.method.split('.').nth(1).unwrap_or_default()
84 }
85
86 #[must_use]
88 pub fn parse(&self) -> ParsedEvent {
89 self.parse_internal()
90 }
91}
92
93#[derive(Debug, Clone, Serialize)]
111pub struct EventReply {
112 pub id: RequestId,
114
115 #[serde(rename = "replyTo")]
117 pub reply_to: String,
118
119 pub result: Value,
121}
122
123impl EventReply {
124 #[inline]
126 #[must_use]
127 pub fn new(id: RequestId, reply_to: impl Into<String>, result: Value) -> Self {
128 Self {
129 id,
130 reply_to: reply_to.into(),
131 result,
132 }
133 }
134
135 #[inline]
137 #[must_use]
138 pub fn allow(id: RequestId, reply_to: impl Into<String>) -> Self {
139 Self::new(id, reply_to, json!({ "action": "allow" }))
140 }
141
142 #[inline]
144 #[must_use]
145 pub fn block(id: RequestId, reply_to: impl Into<String>) -> Self {
146 Self::new(id, reply_to, json!({ "action": "block" }))
147 }
148
149 #[inline]
151 #[must_use]
152 pub fn redirect(id: RequestId, reply_to: impl Into<String>, url: impl Into<String>) -> Self {
153 Self::new(
154 id,
155 reply_to,
156 json!({ "action": "redirect", "url": url.into() }),
157 )
158 }
159}
160
161#[derive(Debug, Clone)]
167pub enum ParsedEvent {
168 BrowsingContextNavigationStarted {
170 tab_id: u32,
172 frame_id: u64,
174 url: String,
176 },
177
178 BrowsingContextDomContentLoaded {
180 tab_id: u32,
182 frame_id: u64,
184 url: String,
186 },
187
188 BrowsingContextLoad {
190 tab_id: u32,
192 frame_id: u64,
194 url: String,
196 },
197
198 BrowsingContextNavigationFailed {
200 tab_id: u32,
202 frame_id: u64,
204 url: String,
206 error: String,
208 },
209
210 ElementAdded {
212 strategy: String,
214 value: String,
216 element_id: String,
218 subscription_id: String,
220 tab_id: u32,
222 frame_id: u64,
224 },
225
226 ElementRemoved {
228 element_id: String,
230 tab_id: u32,
232 frame_id: u64,
234 },
235
236 ElementAttributeChanged {
238 element_id: String,
240 attribute_name: String,
242 old_value: Option<String>,
244 new_value: Option<String>,
246 tab_id: u32,
248 frame_id: u64,
250 },
251
252 NetworkBeforeRequestSent {
254 request_id: String,
256 url: String,
258 method: String,
260 resource_type: String,
262 },
263
264 NetworkResponseStarted {
266 request_id: String,
268 url: String,
270 status: u16,
272 status_text: String,
274 },
275
276 NetworkResponseCompleted {
278 request_id: String,
280 url: String,
282 status: u16,
284 },
285
286 Unknown {
288 method: String,
290 params: Value,
292 },
293}
294
295impl Event {
300 fn parse_internal(&self) -> ParsedEvent {
302 match self.method.as_str() {
303 "browsingContext.navigationStarted" => ParsedEvent::BrowsingContextNavigationStarted {
304 tab_id: self.get_u32("tabId"),
305 frame_id: self.get_u64("frameId"),
306 url: self.get_string("url"),
307 },
308
309 "browsingContext.domContentLoaded" => ParsedEvent::BrowsingContextDomContentLoaded {
310 tab_id: self.get_u32("tabId"),
311 frame_id: self.get_u64("frameId"),
312 url: self.get_string("url"),
313 },
314
315 "browsingContext.load" => ParsedEvent::BrowsingContextLoad {
316 tab_id: self.get_u32("tabId"),
317 frame_id: self.get_u64("frameId"),
318 url: self.get_string("url"),
319 },
320
321 "browsingContext.navigationFailed" => ParsedEvent::BrowsingContextNavigationFailed {
322 tab_id: self.get_u32("tabId"),
323 frame_id: self.get_u64("frameId"),
324 url: self.get_string("url"),
325 error: self.get_string("error"),
326 },
327
328 "element.added" => ParsedEvent::ElementAdded {
329 strategy: self.get_string("strategy"),
330 value: self.get_string("value"),
331 element_id: self.get_string("elementId"),
332 subscription_id: self.get_string("subscriptionId"),
333 tab_id: self.get_u32("tabId"),
334 frame_id: self.get_u64("frameId"),
335 },
336
337 "element.removed" => ParsedEvent::ElementRemoved {
338 element_id: self.get_string("elementId"),
339 tab_id: self.get_u32("tabId"),
340 frame_id: self.get_u64("frameId"),
341 },
342
343 "element.attributeChanged" => ParsedEvent::ElementAttributeChanged {
344 element_id: self.get_string("elementId"),
345 attribute_name: self.get_string("attributeName"),
346 old_value: self.get_optional_string("oldValue"),
347 new_value: self.get_optional_string("newValue"),
348 tab_id: self.get_u32("tabId"),
349 frame_id: self.get_u64("frameId"),
350 },
351
352 "network.beforeRequestSent" => ParsedEvent::NetworkBeforeRequestSent {
353 request_id: self.get_string("requestId"),
354 url: self.get_string("url"),
355 method: self.get_string_or("method", "GET"),
356 resource_type: self.get_string_or("resourceType", "other"),
357 },
358
359 "network.responseStarted" => ParsedEvent::NetworkResponseStarted {
360 request_id: self.get_string("requestId"),
361 url: self.get_string("url"),
362 status: self.get_u16("status"),
363 status_text: self.get_string("statusText"),
364 },
365
366 "network.responseCompleted" => ParsedEvent::NetworkResponseCompleted {
367 request_id: self.get_string("requestId"),
368 url: self.get_string("url"),
369 status: self.get_u16("status"),
370 },
371
372 _ => ParsedEvent::Unknown {
373 method: self.method.clone(),
374 params: self.params.clone(),
375 },
376 }
377 }
378
379 #[inline]
381 fn get_string(&self, key: &str) -> String {
382 self.params
383 .get(key)
384 .and_then(|v| v.as_str())
385 .unwrap_or_default()
386 .to_string()
387 }
388
389 #[inline]
391 fn get_string_or(&self, key: &str, default: &str) -> String {
392 self.params
393 .get(key)
394 .and_then(|v| v.as_str())
395 .unwrap_or(default)
396 .to_string()
397 }
398
399 #[inline]
401 fn get_optional_string(&self, key: &str) -> Option<String> {
402 self.params
403 .get(key)
404 .and_then(|v| v.as_str())
405 .map(|s| s.to_string())
406 }
407
408 #[inline]
410 fn get_u32(&self, key: &str) -> u32 {
411 self.params
412 .get(key)
413 .and_then(|v| v.as_u64())
414 .unwrap_or_default() as u32
415 }
416
417 #[inline]
419 fn get_u64(&self, key: &str) -> u64 {
420 self.params
421 .get(key)
422 .and_then(|v| v.as_u64())
423 .unwrap_or_default()
424 }
425
426 #[inline]
428 fn get_u16(&self, key: &str) -> u16 {
429 self.params
430 .get(key)
431 .and_then(|v| v.as_u64())
432 .unwrap_or_default() as u16
433 }
434}
435
436#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn test_event_parsing() {
446 let json_str = r#"{
447 "id": "550e8400-e29b-41d4-a716-446655440000",
448 "type": "event",
449 "method": "browsingContext.load",
450 "params": {
451 "tabId": 1,
452 "frameId": 0,
453 "url": "https://example.com"
454 }
455 }"#;
456
457 let event: Event = serde_json::from_str(json_str).expect("parse event");
458 assert_eq!(event.module(), "browsingContext");
459 assert_eq!(event.event_name(), "load");
460
461 let parsed = event.parse();
462 match parsed {
463 ParsedEvent::BrowsingContextLoad {
464 tab_id,
465 frame_id,
466 url,
467 } => {
468 assert_eq!(tab_id, 1);
469 assert_eq!(frame_id, 0);
470 assert_eq!(url, "https://example.com");
471 }
472 _ => panic!("unexpected parsed event type"),
473 }
474 }
475
476 #[test]
477 fn test_event_reply_allow() {
478 let id = RequestId::generate();
479 let reply = EventReply::allow(id, "network.beforeRequestSent");
480 let json = serde_json::to_string(&reply).expect("serialize");
481
482 assert!(json.contains("replyTo"));
483 assert!(json.contains("allow"));
484 }
485
486 #[test]
487 fn test_event_reply_block() {
488 let id = RequestId::generate();
489 let reply = EventReply::block(id, "network.beforeRequestSent");
490 let json = serde_json::to_string(&reply).expect("serialize");
491
492 assert!(json.contains("block"));
493 }
494
495 #[test]
496 fn test_event_reply_redirect() {
497 let id = RequestId::generate();
498 let reply = EventReply::redirect(id, "network.beforeRequestSent", "https://other.com");
499 let json = serde_json::to_string(&reply).expect("serialize");
500
501 assert!(json.contains("redirect"));
502 assert!(json.contains("https://other.com"));
503 }
504
505 #[test]
506 fn test_element_added_parsing() {
507 let json_str = r##"{
508 "id": "550e8400-e29b-41d4-a716-446655440000",
509 "type": "event",
510 "method": "element.added",
511 "params": {
512 "strategy": "css",
513 "value": "#login-form",
514 "elementId": "elem-123",
515 "subscriptionId": "sub-456",
516 "tabId": 1,
517 "frameId": 0
518 }
519 }"##;
520
521 let event: Event = serde_json::from_str(json_str).expect("parse event");
522 let parsed = event.parse();
523
524 match parsed {
525 ParsedEvent::ElementAdded {
526 strategy,
527 value,
528 element_id,
529 ..
530 } => {
531 assert_eq!(strategy, "css");
532 assert_eq!(value, "#login-form");
533 assert_eq!(element_id, "elem-123");
534 }
535 _ => panic!("unexpected parsed event type"),
536 }
537 }
538
539 #[test]
540 fn test_unknown_event() {
541 let json_str = r#"{
542 "id": "550e8400-e29b-41d4-a716-446655440000",
543 "type": "event",
544 "method": "custom.unknownEvent",
545 "params": { "foo": "bar" }
546 }"#;
547
548 let event: Event = serde_json::from_str(json_str).expect("parse event");
549 let parsed = event.parse();
550
551 match parsed {
552 ParsedEvent::Unknown { method, .. } => {
553 assert_eq!(method, "custom.unknownEvent");
554 }
555 _ => panic!("expected Unknown variant"),
556 }
557 }
558}