oa_gateway_bench/
payload.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
//! Bench payloads. Sequence numbers live in the JSON, not in engine headers.

use oa_gateway_testing::fixtures::POSITION_REPORT_JSON;
use serde_json::{json, Value};

/// Which document to send.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PayloadKind {
    /// Small typed message the fixture schema and the engine both understand.
    Ping,
    /// Realistic UCI document used when `xml_baseline` conversion is on.
    PositionReport,
}

impl PayloadKind {
    #[must_use]
    pub(crate) fn topic(self) -> &'static str {
        match self {
            Self::Ping => "demo",
            Self::PositionReport => "PositionReport",
        }
    }

    #[must_use]
    pub(crate) fn type_hint(self) -> &'static str {
        match self {
            Self::Ping => "Ping",
            Self::PositionReport => "PositionReport",
        }
    }
}

/// Builds a JSON payload with `seq` in `n` and optional padding.
#[must_use]
pub(crate) fn render(kind: PayloadKind, seq: u64, min_bytes: usize) -> String {
    match kind {
        PayloadKind::Ping => {
            let mut from = String::new();
            let mut text = ping_json(seq, &from);
            if text.len() < min_bytes {
                from = "x".repeat(min_bytes.saturating_sub(text.len()));
                text = ping_json(seq, &from);
            }
            text
        }
        PayloadKind::PositionReport => {
            let mut value: Value =
                serde_json::from_str(POSITION_REPORT_JSON).expect("PositionReport fixture is JSON");
            value["PositionReport"]["MessageData"]["n"] = json!(seq);
            let mut text = serde_json::to_string(&value).expect("value is serializable");
            if text.len() < min_bytes {
                let pad = "x".repeat(min_bytes.saturating_sub(text.len()));
                value["PositionReport"]["MessageData"]["pad"] = json!(pad);
                text = serde_json::to_string(&value).expect("value is serializable");
            }
            text
        }
    }
}

fn ping_json(seq: u64, from: &str) -> String {
    if from.is_empty() {
        format!(r#"{{"Ping":{{"n":{seq}}}}}"#)
    } else {
        format!(r#"{{"Ping":{{"n":{seq},"from":"{from}"}}}}"#)
    }
}

/// Reads the sequence number stamped by [`render`].
#[must_use]
pub(crate) fn parse_seq(payload: &[u8]) -> Option<u64> {
    let value: Value = serde_json::from_slice(payload).ok()?;
    value
        .pointer("/Ping/n")
        .or_else(|| value.pointer("/PositionReport/MessageData/n"))
        .and_then(Value::as_u64)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ping_round_trips_seq_and_padding() {
        let text = render(PayloadKind::Ping, 42, 64);
        assert!(text.len() >= 64, "{text}");
        assert_eq!(parse_seq(text.as_bytes()), Some(42));
    }

    #[test]
    fn position_report_carries_seq() {
        let text = render(PayloadKind::PositionReport, 7, 1);
        assert_eq!(parse_seq(text.as_bytes()), Some(7));
        assert!(text.contains("PositionReport"));
    }
}