use std::time::UNIX_EPOCH;
use serde_json::{Map, Value};
use crate::frame::AcarsMessage;
#[must_use]
pub fn serialize_message(msg: &AcarsMessage, station_id: Option<&str>) -> String {
let mut obj = Map::new();
let ts = msg
.timestamp
.duration_since(UNIX_EPOCH)
.map_or(0.0, |d| d.as_secs_f64());
obj.insert("timestamp".to_string(), Value::from(ts));
if let Some(id) = station_id.filter(|s| !s.is_empty()) {
obj.insert("station_id".to_string(), Value::from(id));
}
obj.insert("channel".to_string(), Value::from(msg.channel_idx));
obj.insert("freq".to_string(), Value::from(msg.freq_hz / 1e6));
obj.insert("level".to_string(), Value::from(msg.level_db));
obj.insert("error".to_string(), Value::from(msg.error_count));
obj.insert("mode".to_string(), Value::from(byte_to_string(msg.mode)));
obj.insert("label".to_string(), Value::from(label_to_string(msg.label)));
obj.insert("tail".to_string(), Value::from(msg.aircraft.as_str()));
if msg.block_id != 0 {
obj.insert(
"block_id".to_string(),
Value::from(byte_to_string(msg.block_id)),
);
if msg.ack == b'!' {
obj.insert("ack".to_string(), Value::from(false));
} else {
obj.insert("ack".to_string(), Value::from(byte_to_string(msg.ack)));
}
}
if let Some(f) = &msg.flight_id {
obj.insert("flight".to_string(), Value::from(f.as_str()));
}
if let Some(n) = &msg.message_no {
obj.insert("msgno".to_string(), Value::from(n.as_str()));
}
if !msg.text.is_empty() {
obj.insert("text".to_string(), Value::from(msg.text.as_str()));
}
if msg.end_of_message {
obj.insert("end".to_string(), Value::from(true));
}
if msg.reassembled_block_count > 1 {
obj.insert(
"reassembled_blocks".to_string(),
Value::from(msg.reassembled_block_count),
);
}
if let Some(oooi) = &msg.parsed {
if let Some(v) = &oooi.sa {
obj.insert("depa".to_string(), Value::from(v.as_str()));
}
if let Some(v) = &oooi.da {
obj.insert("dsta".to_string(), Value::from(v.as_str()));
}
if let Some(v) = &oooi.eta {
obj.insert("eta".to_string(), Value::from(v.as_str()));
}
if let Some(v) = &oooi.gout {
obj.insert("gtout".to_string(), Value::from(v.as_str()));
}
if let Some(v) = &oooi.gin {
obj.insert("gtin".to_string(), Value::from(v.as_str()));
}
if let Some(v) = &oooi.woff {
obj.insert("wloff".to_string(), Value::from(v.as_str()));
}
if let Some(v) = &oooi.won {
obj.insert("wlin".to_string(), Value::from(v.as_str()));
}
}
let mut app = Map::new();
app.insert("name".to_string(), Value::from(env!("CARGO_PKG_NAME")));
app.insert("ver".to_string(), Value::from(env!("CARGO_PKG_VERSION")));
obj.insert("app".to_string(), Value::Object(app));
Value::Object(obj).to_string()
}
fn byte_to_string(b: u8) -> String {
let c = b as char;
let mut s = String::with_capacity(1);
s.push(c);
s
}
fn label_to_string(label: [u8; 2]) -> String {
let mut s = String::with_capacity(2);
s.push(label[0] as char);
s.push(label[1] as char);
s
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use std::time::{Duration, UNIX_EPOCH};
use arrayvec::ArrayString;
use serde_json::Value;
use super::*;
use crate::frame::AcarsMessage;
fn make_uplink_msg() -> AcarsMessage {
AcarsMessage {
timestamp: UNIX_EPOCH + Duration::from_secs(1_700_000_000),
channel_idx: 2,
freq_hz: 131_550_000.0,
level_db: 12.0,
error_count: 0,
mode: b'2',
label: *b"H1",
block_id: 0,
ack: 0x15,
aircraft: ArrayString::from(".N12345").unwrap(),
flight_id: None,
message_no: None,
text: String::new(),
end_of_message: true,
reassembled_block_count: 1,
parsed: None,
}
}
#[test]
#[allow(clippy::float_cmp)]
fn serializes_minimal_uplink_message() {
let msg = make_uplink_msg();
let out = serialize_message(&msg, None);
let v: Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["timestamp"].as_f64().unwrap(), 1_700_000_000.0);
assert_eq!(v["channel"].as_u64().unwrap(), 2);
assert!((v["freq"].as_f64().unwrap() - 131.55).abs() < 1e-6);
assert!((v["level"].as_f64().unwrap() - 12.0).abs() < 1e-6);
assert_eq!(v["error"].as_u64().unwrap(), 0);
assert_eq!(v["mode"].as_str().unwrap(), "2");
assert_eq!(v["label"].as_str().unwrap(), "H1");
assert_eq!(v["tail"].as_str().unwrap(), ".N12345");
assert_eq!(v["app"]["name"].as_str().unwrap(), env!("CARGO_PKG_NAME"));
assert!(v["app"]["ver"].is_string());
assert!(v.get("station_id").is_none());
assert!(v.get("block_id").is_none());
assert!(v.get("flight").is_none());
assert!(v.get("text").is_none());
}
#[test]
fn omits_station_id_when_none_or_empty() {
let msg = make_uplink_msg();
let out_none = serialize_message(&msg, None);
let out_empty = serialize_message(&msg, Some(""));
let v_none: Value = serde_json::from_str(&out_none).unwrap();
let v_empty: Value = serde_json::from_str(&out_empty).unwrap();
assert!(v_none.get("station_id").is_none());
assert!(v_empty.get("station_id").is_none());
}
#[test]
fn includes_station_id_when_set() {
let msg = make_uplink_msg();
let out = serialize_message(&msg, Some("ABCD"));
let v: Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["station_id"].as_str().unwrap(), "ABCD");
}
fn make_downlink_msg() -> AcarsMessage {
let mut m = make_uplink_msg();
m.block_id = b'1';
m.ack = b'\x15';
m.flight_id = Some(ArrayString::from("UA1234").unwrap());
m.message_no = Some(ArrayString::from("M01A").unwrap());
m.text = "REPORT".to_string();
m
}
#[test]
fn serializes_full_downlink_message() {
let msg = make_downlink_msg();
let out = serialize_message(&msg, Some("STN1"));
let v: Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["block_id"].as_str().unwrap(), "1");
assert_eq!(v["ack"].as_str().unwrap(), "\x15");
assert_eq!(v["flight"].as_str().unwrap(), "UA1234");
assert_eq!(v["msgno"].as_str().unwrap(), "M01A");
assert_eq!(v["station_id"].as_str().unwrap(), "STN1");
}
#[test]
fn omits_block_id_and_ack_when_block_id_zero() {
let mut msg = make_downlink_msg();
msg.block_id = 0;
let out = serialize_message(&msg, None);
let v: Value = serde_json::from_str(&out).unwrap();
assert!(v.get("block_id").is_none());
assert!(v.get("ack").is_none());
}
#[test]
fn ack_serializes_as_false_when_bang() {
let mut msg = make_downlink_msg();
msg.ack = b'!';
let out = serialize_message(&msg, None);
let v: Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["ack"], Value::from(false));
}
#[test]
fn omits_flight_and_msgno_for_uplink() {
let msg = make_uplink_msg(); let out = serialize_message(&msg, None);
let v: Value = serde_json::from_str(&out).unwrap();
assert!(v.get("flight").is_none());
assert!(v.get("msgno").is_none());
}
#[test]
fn omits_empty_text_field() {
let msg = make_uplink_msg(); let out = serialize_message(&msg, None);
let v: Value = serde_json::from_str(&out).unwrap();
assert!(v.get("text").is_none());
}
#[test]
fn end_field_only_when_end_of_message() {
let mut msg = make_uplink_msg();
msg.end_of_message = false;
let out = serialize_message(&msg, None);
let v: Value = serde_json::from_str(&out).unwrap();
assert!(v.get("end").is_none());
msg.end_of_message = true;
let out = serialize_message(&msg, None);
let v: Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["end"], Value::from(true));
}
#[test]
fn reassembled_blocks_field_only_when_gt_one() {
let mut msg = make_uplink_msg();
msg.reassembled_block_count = 1;
let out = serialize_message(&msg, None);
let v: Value = serde_json::from_str(&out).unwrap();
assert!(v.get("reassembled_blocks").is_none());
msg.reassembled_block_count = 3;
let out = serialize_message(&msg, None);
let v: Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["reassembled_blocks"].as_u64().unwrap(), 3);
}
#[test]
fn oooi_fields_appear_when_parsed_some() {
use crate::label_parsers::Oooi;
let mut msg = make_downlink_msg();
msg.parsed = Some(Oooi {
sa: Some(ArrayString::from("KORD").unwrap()),
da: Some(ArrayString::from("KSFO").unwrap()),
eta: Some(ArrayString::from("0830").unwrap()),
gout: Some(ArrayString::from("0700").unwrap()),
gin: None,
woff: Some(ArrayString::from("0715").unwrap()),
won: Some(ArrayString::from("1015").unwrap()),
});
let out = serialize_message(&msg, None);
let v: Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["depa"].as_str().unwrap(), "KORD");
assert_eq!(v["dsta"].as_str().unwrap(), "KSFO");
assert_eq!(v["eta"].as_str().unwrap(), "0830");
assert_eq!(v["gtout"].as_str().unwrap(), "0700");
assert_eq!(v["wloff"].as_str().unwrap(), "0715");
assert_eq!(v["wlin"].as_str().unwrap(), "1015");
assert!(v.get("gtin").is_none()); }
#[test]
fn oooi_fields_omitted_when_parsed_none() {
let msg = make_uplink_msg();
assert!(msg.parsed.is_none());
let out = serialize_message(&msg, None);
let v: Value = serde_json::from_str(&out).unwrap();
for key in ["depa", "dsta", "eta", "gtout", "gtin", "wloff", "wlin"] {
assert!(v.get(key).is_none(), "{key} should be absent");
}
}
}