use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct HandshakeParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HandshakeInfo {
pub app_name: String,
pub frust_version: String,
pub protocol_version: u32,
pub capabilities: Vec<Capability>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Capability {
WidgetTree,
FrameStats,
Input,
Metrics,
Screenshot,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WidgetTreeDump {
pub roots: Vec<WidgetNode>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WidgetNode {
pub id: u64,
pub type_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub debug_label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bounds: Option<RectPx>,
#[serde(default)]
pub children: Vec<WidgetNode>,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct RectPx {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct WidgetPropsParams {
pub id: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WidgetProps {
pub id: u64,
pub entries: Vec<(String, String)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrameStats {
pub n: u64,
pub total_us: u64,
pub rebuild_us: u64,
pub layout_us: u64,
pub paint_us: u64,
pub encode_us: u64,
pub acquire_us: u64,
pub submit_us: u64,
pub skipped: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct MetricsSnapshot {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rss_bytes: Option<u64>,
pub uptime_ms: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct InputTapParams {
pub x: f64,
pub y: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct InputScrollParams {
pub x: f64,
pub y: f64,
pub dx: f64,
pub dy: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InputTextParams {
pub text: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct AckResult {
#[serde(default = "default_true")]
pub ok: bool,
}
fn default_true() -> bool {
true
}
impl Default for AckResult {
fn default() -> Self {
Self { ok: true }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ScreenshotResult {
pub png_base64: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn handshake_params_round_trip_with_and_without_a_token() {
let with = HandshakeParams {
token: Some("0123456789abcdef".to_string()),
};
let json = serde_json::to_string(&with).unwrap();
assert_eq!(
serde_json::from_str::<HandshakeParams>(&json).unwrap(),
with
);
let without: HandshakeParams = serde_json::from_str("{}").unwrap();
assert_eq!(without, HandshakeParams::default());
assert_eq!(without.token, None);
assert!(!serde_json::to_string(&without).unwrap().contains("token"));
}
#[test]
fn no_server_written_payload_carries_a_token_field() {
let info = HandshakeInfo {
app_name: "app".to_string(),
frust_version: "0.1.0".to_string(),
protocol_version: 1,
capabilities: vec![Capability::WidgetTree],
};
let json = serde_json::to_string(&info).unwrap();
assert!(!json.contains("token"));
}
#[test]
fn capability_round_trips() {
for cap in [
Capability::WidgetTree,
Capability::FrameStats,
Capability::Input,
Capability::Metrics,
Capability::Screenshot,
] {
let json = serde_json::to_string(&cap).unwrap();
let back: Capability = serde_json::from_str(&json).unwrap();
assert_eq!(cap, back);
}
}
#[test]
fn unknown_capability_deserializes_to_unknown_variant() {
let cap: Capability = serde_json::from_str("\"some_future_capability\"").unwrap();
assert_eq!(cap, Capability::Unknown);
}
#[test]
fn ack_result_defaults_ok_true_when_field_omitted() {
let ack: AckResult = serde_json::from_str("{}").unwrap();
assert_eq!(ack, AckResult { ok: true });
assert_eq!(AckResult::default(), AckResult { ok: true });
}
#[test]
fn frame_stats_field_names_match_perf_raw_line() {
let stats = FrameStats {
n: 42,
total_us: 1_000,
rebuild_us: 100,
layout_us: 200,
paint_us: 300,
encode_us: 150,
acquire_us: 50,
submit_us: 200,
skipped: false,
};
let json = serde_json::to_value(stats).unwrap();
for key in [
"n",
"total_us",
"rebuild_us",
"layout_us",
"paint_us",
"encode_us",
"acquire_us",
"submit_us",
"skipped",
] {
assert!(json.get(key).is_some(), "missing field: {key}");
}
}
}