[refactor](trx-frontend-http): replace string-level JSON splice with serde(flatten)

Use a StateWithMeta wrapper struct with #[serde(flatten)] for merging
rig state with frontend meta, replacing the manual string manipulation.
Also add Serialize derive and skip_serializing_if to FrontendMeta.

https://claude.ai/code/session_01XzurkeuUmamBuhQwxVy7T4
Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-25 22:43:01 +00:00
committed by Stan Grams
parent 635a1214d0
commit cbe22bd7b6
@@ -80,15 +80,23 @@ fn encode_spectrum_frame(frame: &trx_core::rig::state::SpectrumData) -> String {
format!("{},{},{b64}", frame.center_hz, frame.sample_rate) format!("{},{},{b64}", frame.center_hz, frame.sample_rate)
} }
#[derive(serde::Serialize)]
struct FrontendMeta { struct FrontendMeta {
#[serde(rename = "clients")]
http_clients: usize, http_clients: usize,
rigctl_clients: usize, rigctl_clients: usize,
#[serde(skip_serializing_if = "Option::is_none")]
rigctl_addr: Option<String>, rigctl_addr: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
active_remote: Option<String>, active_remote: Option<String>,
remotes: Vec<String>, remotes: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
owner_callsign: Option<String>, owner_callsign: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
owner_website_url: Option<String>, owner_website_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
owner_website_name: Option<String>, owner_website_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
ais_vessel_url_base: Option<String>, ais_vessel_url_base: Option<String>,
show_sdr_gain_control: bool, show_sdr_gain_control: bool,
initial_map_zoom: u8, initial_map_zoom: u8,
@@ -98,6 +106,16 @@ struct FrontendMeta {
server_connected: bool, server_connected: bool,
} }
/// Wrapper that flattens a rig state with frontend meta into a single JSON
/// object, replacing the old string-level splice approach.
#[derive(serde::Serialize)]
struct StateWithMeta<'a> {
#[serde(flatten)]
state: &'a serde_json::Value,
#[serde(flatten)]
meta: &'a FrontendMeta,
}
/// Tracks per-SSE-session rig selection so different browser tabs can /// Tracks per-SSE-session rig selection so different browser tabs can
/// independently view different rigs without interfering. /// independently view different rigs without interfering.
#[derive(Default)] #[derive(Default)]
@@ -166,76 +184,21 @@ pub async fn status_api(
.body(json)) .body(json))
} }
/// Append frontend meta fields to an already-serialised JSON object string. /// Merge a rig state JSON string with frontend meta via `#[serde(flatten)]`.
/// ///
/// Avoids a full parse→modify→reserialise cycle (two serde round-trips per /// Parses the state once into a `serde_json::Value`, then serializes the
/// event) by working directly at the string level: strip the closing `}`, /// combined `StateWithMeta` wrapper in a single pass — cleaner and faster
/// serialize only the extra fields once, and re-close the object. /// than the old string-level splice approach.
fn inject_frontend_meta(json: &str, meta: FrontendMeta) -> String { fn inject_frontend_meta(json: &str, meta: FrontendMeta) -> String {
let trimmed = json.trim_end(); let state: serde_json::Value = match serde_json::from_str(json) {
let Some(base) = trimmed.strip_suffix('}') else { Ok(v) => v,
return json.to_string();
};
// Build only the extra key-value pairs as a JSON fragment.
let mut extra = serde_json::Map::new();
extra.insert("clients".into(), serde_json::json!(meta.http_clients));
extra.insert(
"rigctl_clients".into(),
serde_json::json!(meta.rigctl_clients),
);
if let Some(v) = meta.rigctl_addr {
extra.insert("rigctl_addr".into(), serde_json::json!(v));
}
if let Some(v) = meta.active_remote {
extra.insert("active_remote".into(), serde_json::json!(v));
}
extra.insert("remotes".into(), serde_json::json!(meta.remotes));
if let Some(v) = meta.owner_callsign {
extra.insert("owner_callsign".into(), serde_json::json!(v));
}
if let Some(v) = meta.owner_website_url {
extra.insert("owner_website_url".into(), serde_json::json!(v));
}
if let Some(v) = meta.owner_website_name {
extra.insert("owner_website_name".into(), serde_json::json!(v));
}
if let Some(v) = meta.ais_vessel_url_base {
extra.insert("ais_vessel_url_base".into(), serde_json::json!(v));
}
extra.insert(
"show_sdr_gain_control".into(),
serde_json::json!(meta.show_sdr_gain_control),
);
extra.insert(
"initial_map_zoom".into(),
serde_json::json!(meta.initial_map_zoom),
);
extra.insert(
"spectrum_coverage_margin_hz".into(),
serde_json::json!(meta.spectrum_coverage_margin_hz),
);
extra.insert(
"spectrum_usable_span_ratio".into(),
serde_json::json!(meta.spectrum_usable_span_ratio),
);
extra.insert(
"decode_history_retention_min".into(),
serde_json::json!(meta.decode_history_retention_min),
);
extra.insert(
"server_connected".into(),
serde_json::json!(meta.server_connected),
);
// Serialize the extra map, strip its outer braces, and splice in.
let extra_json = match serde_json::to_string(&extra) {
Ok(s) => s,
Err(_) => return json.to_string(), Err(_) => return json.to_string(),
}; };
// extra_json = {"k":v,...} → strip { and } let combined = StateWithMeta {
let inner = &extra_json[1..extra_json.len() - 1]; state: &state,
format!("{base},{inner}}}") meta: &meta,
};
serde_json::to_string(&combined).unwrap_or_else(|_| json.to_string())
} }
fn frontend_meta_from_context( fn frontend_meta_from_context(