A high-performance, zero-allocation, fully compile-safe Rust representation of the Chrome DevTools JavaScript Protocol (js_protocol), generated directly from the official protocol definitions.
Most auto-generated CDP crates expose raw, unidiomatic APIs with substantial runtime allocation overhead. This library is designed from the ground up to solve those problems.
All generated fields, getters, and builder setters are translated from the protocol's raw camelCase to standard Rust snake_case (for example, executionContextId becomes execution_context_id and objectGroup becomes object_group). #[serde(rename = "...")] attributes ensure the serialized JSON matches the exact wire format Chrome expects.
String properties use Cow<'a, str> instead of allocating a String. Builder arguments use impl Into<...>, so you can pass static string literals (&str) or owned strings without unnecessary heap allocations.
The builder pattern separates required from optional parameters. Required parameters are passed directly to builder(...), so protocol compliance is checked at compile time:
// `expression` is required (passed to builder); `silent` is optional (chained).
let params = EvaluateParams::builder("1 + 1")
.silent(true)
.build();Getters, builders, command glue, and event glue are all synthesized by derives from js-protocol-macros, so the generated source is limited to plain struct definitions. This removes roughly 60% of the generated source compared to hand-written builders and getters, while still exposing every typed command and event. Runtime dependencies remain limited to serde and serde_json.
The crate includes no WebSocket client and does not require a specific async runtime. It is compatible with any runtime or network stack.
[dependencies]
js-protocol = { version = "0.1.6", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"use js_protocol::runtime::EvaluateParams;
fn main() {
// Build the command parameters.
let params = EvaluateParams::builder("console.log('Hello from Rust!')")
.silent(true)
.build();
// Read-only getters.
println!("Expression to run: {}", params.expression());
// Serialize to the wire protocol payload (unset Option fields are skipped).
let payload = serde_json::to_string(¶ms).unwrap();
println!("Payload: {}", payload);
// Output: {"expression":"console.log('Hello from Rust!')","silent":true}
}Every parameter struct implements crate::CdpCommand<'a>, which binds it to its command method and its corresponding response type:
use js_protocol::runtime::CallFunctionOnParams;
use js_protocol::CdpCommand;
fn call_function() {
let params = CallFunctionOnParams::builder("function() { return this; }")
.object_id("object-1")
.build();
// The trait binds this command to its method name and response type.
assert_eq!(CallFunctionOnParams::METHOD, "Runtime.callFunctionOn");
// In your network client:
// let response_json = websocket.send_command(CallFunctionOnParams::METHOD, ¶ms).await;
// let response: CallFunctionOnReturns = serde_json::from_str(&response_json).unwrap();
}Because every command implements CdpCommand, a single pair of helpers covers all of them:
use serde::{de::DeserializeOwned, Serialize};
use js_protocol::{CdpCommand, Command, Response};
fn encode<'a, P: CdpCommand<'a> + Serialize>(id: u64, params: &'a P) -> String {
serde_json::to_string(&Command::new(id, params)).unwrap()
}
fn decode<'a, P>(json: &'a str) -> Response<P::Response>
where
P: CdpCommand<'a>,
P::Response: DeserializeOwned,
{
serde_json::from_str(json).unwrap()
}Three derives from js-protocol-macros keep every generated type down to a plain struct declaration.
Every generated type derives CdpBuilder, which emits:
-
A
builder(...)constructor where every non-Optionfield is a required argument (typed asimpl Into<FieldType>). -
Chainable setters for every
Optionfield, wrapping the value inSome. -
A
build()method that moves the accumulated fields into the struct. -
Read-only getters whose return type is chosen from the field type:
Field type Getter return Cow<'a, str>/Option<Cow<'a, str>>&str/Option<&str>Vec<T>/Option<Vec<T>>&[T]/Option<&[T]>Box<T>/Option<Box<T>>&T/Option<&T>numeric / boolprimitivesthe value itself (they are Copy)any other type T&T/Option<&T>
A command's parameter struct carries its method name and response type instead of a hand-written impl:
#[derive(CdpBuilder, CdpCommand)]
#[cdp(method = "Runtime.evaluate", response = "EvaluateReturns<'a>")]
pub struct EvaluateParams<'a> { /* fields */ }This generates EvaluateParams::METHOD and the CdpCommand<'a> implementation. If response is omitted, the reply type defaults to crate::EmptyReturns.
Every event in the schema becomes a typed struct with the same builder and getters:
#[derive(CdpBuilder, CdpEvent)]
#[cdp(method = "Runtime.consoleAPICalled")]
pub struct ConsoleAPICalled<'a> { /* fields */ }
assert_eq!(ConsoleAPICalled::METHOD, "Runtime.consoleAPICalled");The CdpEvent trait exposes METHOD, so events can be handled generically rather than by matching raw JSON.
Response<T> decodes {"id", "result"}. For the failure path, CdpReply<T> decodes either shape:
match serde_json::from_str::<CdpReply<EvaluateReturns>>(raw)? {
CdpReply::Ok(reply) => use_result(reply.result.result()),
CdpReply::Err(err) => eprintln!("CDP {}: {}", err.error.code, err.error.message),
}The code is generated by a single Python script that performs schema analysis:
-
Fixed-point lifetime propagation: The generator iteratively analyzes types, command parameters/returns, and events to detect circular references, nesting, and dependency hierarchies. It determines which types require a lifetime parameter (
<'a>) and wraps recursive structures inBoxto prevent infinite-size compile errors. -
HTML and Markdown escaping: Schema documentation contains raw Markdown and HTML brackets. The generator escapes them into valid rustdoc, keeping compilation warning-free.
-
Per-domain feature flags: Every JavaScript protocol domain is a Rust feature. You can limit compile times to the domains you need:
# Compile only the runtime and debugger domains. js-protocol = { version = "0.1.6", default-features = false, features = ["runtime", "debugger"] }
# Exit code 0 = up to date, 1 = a newer protocol is available.
python scripts/generate_rust_code.py --check
# Download the latest protocol and regenerate everything.
python scripts/generate_rust_code.py --download--download is optional; if a local js_protocol.json is present it is used as-is.
python scripts/generate_rust_code.py --version 0.1.6cargo build --workspace
cargo test --workspace
cargo clippy --workspace --all-targets --all-features -- -D warningsLint policy is defined in the workspace Cargo.toml ([workspace.lints]) and clippy.toml. Clippy warnings are treated as errors in CI. A small set of lints that are not meaningful for generated code (empty_line_after_doc_comments, doc_lazy_continuation, and vec_box) is allowed explicitly; everything else is held to the default Clippy standard.
Versions are kept in lockstep by the generator, and releases are cut from a tag:
# 1. Bump every version reference at once (both Cargo.toml files, the macros pin, README).
python scripts/generate_rust_code.py --version 0.1.6
# 2. Verify.
cargo test --workspace --all-features
cargo clippy --workspace --all-targets --all-features -- -D warnings
python scripts/publish.py --dry-run
# 3. Commit, tag, push. The tag triggers .github/workflows/release.yml.
git add -A
git commit -m "Release v0.1.6"
git tag v0.1.6
git push origin main
git push origin v0.1.6The release workflow runs the build/test/clippy gates and then scripts/publish.py, which publishes js-protocol-macros before js-protocol and skips any version already on crates.io, so a partially failed run can be re-run safely. Publishing uses crates.io trusted publishing; both crates must have a trusted publisher configured for this repository and the release.yml workflow.
js-protocol/ # The crate: generated modules + CdpCommand / CdpEvent traits
src/<domain>/mod.rs # One module per CDP domain
scripts/
generate_rust_code.py # Schema analysis and code generation
macros/ # js-protocol-macros: CdpBuilder / CdpCommand / CdpEvent derives
Publishing note:
js-protocoldepends onjs-protocol-macros, so the macros crate must be published to crates.io first.
Distributed under the MIT License. See LICENSE for more information.