use std::any::Any;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use serde_json::Value;
use crate::{CdpError, DomainHandler, EventSender};
pub struct DomainRegistry<H: DomainHandler> {
handlers: Mutex<HashMap<&'static str, H>>,
}
pub struct EmptyHandler;
impl DomainHandler for EmptyHandler {
fn domain_name(&self) -> &'static str {
""
}
fn handle_command(&self, _: &str, _: Value, _: &dyn EventSender) -> Result<Value, CdpError> {
Err(CdpError {
code: -32601,
message: "empty handler".into(),
})
}
}
pub trait RegistryDispatch: Send + Sync + 'static {
fn dispatch_command(
&self,
method: &str,
params: Value,
event_sender: &dyn EventSender,
) -> Option<Result<Value, CdpError>>;
fn dispatch_message(
&self,
msg: &crate::protocol::CdpMessage,
ws_target_id: &str,
event_sender: &dyn EventSender,
) -> Option<Result<Value, CdpError>> {
let _ = ws_target_id;
self.dispatch_command(
&msg.method,
msg.params.clone().unwrap_or_default(),
event_sender,
)
}
fn notify_session_created(&self, domain: &str, session_id: &str);
fn notify_session_destroyed(&self, domains: &[String], session_id: &str);
fn has_domain(&self, domain: &str) -> bool;
fn as_any(&self) -> &dyn Any;
}
impl<H: DomainHandler + 'static> RegistryDispatch for DomainRegistry<H> {
fn dispatch_command(
&self,
method: &str,
params: Value,
event_sender: &dyn EventSender,
) -> Option<Result<Value, CdpError>> {
DomainRegistry::dispatch_command(self, method, params, event_sender)
}
fn notify_session_created(&self, domain: &str, session_id: &str) {
DomainRegistry::notify_session_created(self, domain, session_id)
}
fn notify_session_destroyed(&self, domains: &[String], session_id: &str) {
DomainRegistry::notify_session_destroyed(self, domains, session_id)
}
fn has_domain(&self, domain: &str) -> bool {
DomainRegistry::has_domain(self, domain)
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl dyn RegistryDispatch {
pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
self.as_any().downcast_ref::<T>()
}
}
impl<H: DomainHandler> Default for DomainRegistry<H> {
fn default() -> Self {
Self::new()
}
}
impl<H: DomainHandler> DomainRegistry<H> {
pub fn new() -> Self {
DomainRegistry {
handlers: Mutex::new(HashMap::new()),
}
}
pub fn register(&self, handler: H) -> Result<(), String> {
let mut map = self
.handlers
.lock()
.map_err(|_| -> String { "lock poisoned".into() })?;
let name = handler.domain_name();
if map.contains_key(name) {
return Err(format!("domain '{}' already registered", name));
}
map.insert(name, handler);
Ok(())
}
pub fn dispatch_command(
&self,
method: &str,
params: Value,
event_sender: &dyn EventSender,
) -> Option<Result<Value, CdpError>> {
let domain = method.split('.').next().unwrap_or("");
let map = self.handlers.lock().ok()?;
let handler = map.get(domain)?;
Some(handler.handle_command(method, params, event_sender))
}
pub fn notify_session_created(&self, domain: &str, session_id: &str) {
if let Ok(map) = self.handlers.lock() {
if let Some(handler) = map.get(domain) {
handler.on_session_created(session_id);
}
}
}
pub fn notify_session_destroyed(&self, domains: &[String], session_id: &str) {
if let Ok(map) = self.handlers.lock() {
for domain in domains {
if let Some(handler) = map.get(domain.as_str()) {
handler.on_session_destroyed(session_id);
}
}
}
}
pub fn has_domain(&self, domain: &str) -> bool {
self.handlers
.lock()
.map(|m| m.contains_key(domain))
.unwrap_or(false)
}
}
pub type SharedRegistry = Arc<dyn RegistryDispatch>;
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::{CdpError, DomainHandler, EventSender};
struct NoopSender;
impl EventSender for NoopSender {
fn send_event(&self, _method: &str, _params: Value) {}
}
struct MockHandler {
name: &'static str,
}
impl DomainHandler for MockHandler {
fn domain_name(&self) -> &'static str {
self.name
}
fn handle_command(
&self,
_command: &str,
_params: Value,
_event_sender: &dyn EventSender,
) -> Result<Value, CdpError> {
Ok(json!({}))
}
fn on_session_created(&self, _session_id: &str) {}
fn on_session_destroyed(&self, _session_id: &str) {}
}
#[test]
fn new_registry_is_empty() {
let reg = DomainRegistry::<MockHandler>::new();
assert!(!reg.has_domain("Page"));
}
#[test]
fn default_same_as_new() {
let via_new = DomainRegistry::<MockHandler>::new();
let via_default = DomainRegistry::<MockHandler>::default();
assert!(!via_new.has_domain("Page"));
assert!(!via_default.has_domain("Page"));
}
#[test]
fn register_handler_then_has_domain() {
let reg = DomainRegistry::<MockHandler>::new();
reg.register(MockHandler { name: "Page" }).unwrap();
assert!(reg.has_domain("Page"));
}
#[test]
fn register_duplicate_returns_err() {
let reg = DomainRegistry::<MockHandler>::new();
reg.register(MockHandler { name: "Page" }).unwrap();
let err = reg.register(MockHandler { name: "Page" }).unwrap_err();
assert!(err.contains("'Page'"));
}
#[test]
fn register_different_domains_both_present() {
let reg = DomainRegistry::<MockHandler>::new();
reg.register(MockHandler { name: "Page" }).unwrap();
reg.register(MockHandler { name: "Runtime" }).unwrap();
assert!(reg.has_domain("Page"));
assert!(reg.has_domain("Runtime"));
}
#[test]
fn has_domain_unregistered_returns_false() {
let reg = DomainRegistry::<MockHandler>::new();
reg.register(MockHandler { name: "Page" }).unwrap();
assert!(!reg.has_domain("DOM"));
}
#[test]
fn dispatch_command_registered_returns_some_ok() {
let reg = DomainRegistry::<MockHandler>::new();
reg.register(MockHandler { name: "Page" }).unwrap();
let result = reg.dispatch_command("Page.navigate", json!(null), &NoopSender);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
}
#[test]
fn dispatch_command_unregistered_returns_none() {
let reg = DomainRegistry::<MockHandler>::new();
let result = reg.dispatch_command("DOM.getDocument", json!(null), &NoopSender);
assert!(result.is_none());
}
#[test]
fn dispatch_command_extracts_domain_from_method() {
let reg = DomainRegistry::<MockHandler>::new();
reg.register(MockHandler { name: "Runtime" }).unwrap();
let result = reg.dispatch_command("Runtime.evaluate", json!(null), &NoopSender);
assert!(result.is_some());
}
#[test]
fn notify_session_created_unregistered_no_panic() {
let reg = DomainRegistry::<MockHandler>::new();
reg.notify_session_created("Page", "sess-1");
}
#[test]
fn notify_session_destroyed_unregistered_no_panic() {
let reg = DomainRegistry::<MockHandler>::new();
reg.notify_session_destroyed(&["Page".to_string()], "sess-1");
}
#[test]
fn dispatch_command_with_valid_json_params() {
let reg = DomainRegistry::<MockHandler>::new();
reg.register(MockHandler { name: "Page" }).unwrap();
let result = reg.dispatch_command(
"Page.navigate",
json!({ "url": "https://example.com" }),
&NoopSender,
);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
}
#[test]
fn enum_dispatch_with_concrete_type() {
enum TestDispatch {
Page(MockHandler),
Runtime(MockHandler),
}
impl DomainHandler for TestDispatch {
fn domain_name(&self) -> &'static str {
match self {
Self::Page(h) => h.domain_name(),
Self::Runtime(h) => h.domain_name(),
}
}
fn handle_command(
&self,
cmd: &str,
p: Value,
s: &dyn EventSender,
) -> Result<Value, CdpError> {
match self {
Self::Page(h) => h.handle_command(cmd, p, s),
Self::Runtime(h) => h.handle_command(cmd, p, s),
}
}
}
let reg = DomainRegistry::<TestDispatch>::new();
reg.register(TestDispatch::Page(MockHandler { name: "Page" }))
.unwrap();
reg.register(TestDispatch::Runtime(MockHandler { name: "Runtime" }))
.unwrap();
assert!(reg.has_domain("Page"));
assert!(reg.has_domain("Runtime"));
let result = reg.dispatch_command("Page.navigate", json!(null), &NoopSender);
assert!(result.is_some());
assert!(result.unwrap().is_ok());
}
}