use serde::{Deserialize, Serialize};
pub trait CdpCommand: Serialize {
const METHOD: &'static str;
type Response: for<'de> Deserialize<'de>;
}
#[cfg(feature = "webview")]
#[derive(Debug, Serialize)]
pub struct RuntimeEnable {}
#[cfg(feature = "webview")]
impl CdpCommand for RuntimeEnable {
const METHOD: &'static str = "Runtime.enable";
type Response = Empty;
}
#[cfg(feature = "webview")]
#[derive(Debug, Serialize)]
pub struct AddBinding {
pub name: &'static str,
}
#[cfg(feature = "webview")]
impl CdpCommand for AddBinding {
const METHOD: &'static str = "Runtime.addBinding";
type Response = Empty;
}
#[cfg(feature = "webview")]
#[derive(Debug, Serialize)]
pub struct PageEnable {}
#[cfg(feature = "webview")]
impl CdpCommand for PageEnable {
const METHOD: &'static str = "Page.enable";
type Response = Empty;
}
#[cfg(feature = "webview")]
#[derive(Debug, Serialize)]
pub struct AddScriptToEvaluateOnNewDocument<'a> {
pub source: &'a str,
}
#[cfg(feature = "webview")]
impl CdpCommand for AddScriptToEvaluateOnNewDocument<'_> {
const METHOD: &'static str = "Page.addScriptToEvaluateOnNewDocument";
type Response = ScriptIdentifier;
}
#[cfg(feature = "webview")]
#[derive(Debug, Deserialize)]
pub struct ScriptIdentifier {
pub identifier: String,
}
#[cfg(feature = "webview")]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RemoveScriptToEvaluateOnNewDocument<'a> {
pub identifier: &'a str,
}
#[cfg(feature = "webview")]
impl CdpCommand for RemoveScriptToEvaluateOnNewDocument<'_> {
const METHOD: &'static str = "Page.removeScriptToEvaluateOnNewDocument";
type Response = Empty;
}
#[cfg(feature = "webview")]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Evaluate<'a> {
pub expression: &'a str,
pub await_promise: bool,
pub return_by_value: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub context_id: Option<i64>,
}
#[cfg(feature = "webview")]
impl CdpCommand for Evaluate<'_> {
const METHOD: &'static str = "Runtime.evaluate";
type Response = EvaluateResponse;
}
#[cfg(feature = "webview")]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EvaluateResponse {
pub result: RemoteObject,
#[serde(default)]
pub exception_details: Option<ExceptionDetails>,
}
#[cfg(feature = "webview")]
#[derive(Debug, Deserialize)]
pub struct RemoteObject {
#[serde(default)]
pub value: Option<serde_json::Value>,
#[serde(default)]
pub description: Option<String>,
}
#[cfg(feature = "webview")]
#[derive(Debug, Deserialize)]
pub struct ExceptionDetails {
pub text: String,
#[serde(default)]
#[expect(
dead_code,
reason = "`text` carries the message; the value is decoded for completeness"
)]
pub exception: Option<RemoteObject>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SetUserAgentOverride<'a> {
pub user_agent: &'a str,
}
impl CdpCommand for SetUserAgentOverride<'_> {
const METHOD: &'static str = "Network.setUserAgentOverride";
type Response = Empty;
}
#[cfg(feature = "webview")]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SetCookie<'a> {
pub name: &'a str,
pub value: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<&'a str>,
pub secure: bool,
pub http_only: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub same_site: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires: Option<i64>,
}
#[cfg(feature = "webview")]
impl CdpCommand for SetCookie<'_> {
const METHOD: &'static str = "Network.setCookie";
type Response = Empty;
}
#[cfg(feature = "webview")]
#[derive(Debug, Serialize)]
pub struct GetCookies<'a> {
pub urls: Vec<&'a str>,
}
#[cfg(feature = "webview")]
impl CdpCommand for GetCookies<'_> {
const METHOD: &'static str = "Network.getCookies";
type Response = GetCookiesResponse;
}
#[cfg(feature = "webview")]
#[derive(Debug, Deserialize)]
pub struct GetCookiesResponse {
pub cookies: Vec<Cookie>,
}
#[cfg(feature = "webview")]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Cookie {
pub name: String,
pub value: String,
pub domain: String,
pub path: String,
pub secure: bool,
pub http_only: bool,
#[serde(default)]
pub same_site: Option<String>,
#[serde(default)]
pub expires: f64,
}
#[cfg(feature = "chromium")]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CaptureScreenshot {
pub format: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<u8>,
pub from_surface: bool,
pub capture_beyond_viewport: bool,
}
#[cfg(feature = "chromium")]
impl CdpCommand for CaptureScreenshot {
const METHOD: &'static str = "Page.captureScreenshot";
type Response = CaptureScreenshotResponse;
}
#[cfg(feature = "chromium")]
#[derive(Debug, Deserialize)]
pub struct CaptureScreenshotResponse {
pub data: String,
}
#[cfg(feature = "chromium")]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SetDownloadBehavior<'a> {
pub behavior: &'static str,
pub download_path: &'a str,
}
#[cfg(feature = "chromium")]
impl CdpCommand for SetDownloadBehavior<'_> {
const METHOD: &'static str = "Browser.setDownloadBehavior";
type Response = Empty;
}
#[derive(Debug, Serialize)]
pub struct GetVersion {}
impl CdpCommand for GetVersion {
const METHOD: &'static str = "Browser.getVersion";
type Response = Empty;
}
#[derive(Debug, Deserialize)]
pub struct Empty {}
#[cfg(test)]
mod tests {
#[cfg(feature = "chromium")]
use super::CaptureScreenshot;
#[cfg(feature = "webview")]
use super::{CdpCommand, Cookie, Evaluate, EvaluateResponse, SetCookie};
#[test]
#[cfg(feature = "webview")]
fn parameters_serialize_under_the_names_chromium_expects() {
let evaluate = Evaluate {
expression: "1 + 1",
await_promise: true,
return_by_value: true,
context_id: None,
};
let json = serde_json::to_value(&evaluate).expect("serializes");
assert_eq!(Evaluate::METHOD, "Runtime.evaluate");
assert_eq!(json["expression"], "1 + 1");
assert_eq!(json["awaitPromise"], true);
assert_eq!(json["returnByValue"], true);
assert!(json.get("contextId").is_none());
}
#[test]
#[cfg(feature = "webview")]
fn absent_optional_parameters_are_omitted_rather_than_sent_as_null() {
let cookie = SetCookie {
name: "session",
value: "abc",
domain: None,
url: Some("https://waterui.dev"),
path: None,
secure: true,
http_only: true,
same_site: None,
expires: None,
};
let json = serde_json::to_value(&cookie).expect("serializes");
assert!(json.get("domain").is_none());
assert!(json.get("sameSite").is_none());
assert_eq!(json["httpOnly"], true);
}
#[test]
#[cfg(feature = "chromium")]
fn png_omits_the_quality_parameter_that_only_lossy_formats_take() {
let png = CaptureScreenshot {
format: "png",
quality: None,
from_surface: true,
capture_beyond_viewport: false,
};
let json = serde_json::to_value(&png).expect("serializes");
assert!(json.get("quality").is_none());
}
#[test]
#[cfg(feature = "webview")]
fn a_response_missing_its_optional_fields_still_parses() {
let response: EvaluateResponse = serde_json::from_str(r#"{"result":{}}"#).expect("parses");
assert!(response.result.value.is_none());
assert!(response.exception_details.is_none());
let cookie: Cookie = serde_json::from_str(
r#"{"name":"a","value":"b","domain":"waterui.dev","path":"/","secure":true,"httpOnly":false}"#,
)
.expect("parses");
assert!(cookie.same_site.is_none());
assert!(cookie.expires.abs() < f64::EPSILON);
}
}