Skip to main content

melin_server/
response_encoder.rs

1//! Trading-side [`ResponseEncoder`] implementation.
2//!
3//! Mirror of [`crate::request_decoder::RequestDecoder`] on the
4//! outbound path: maps trading-shaped output payloads
5//! (`ExecutionReport`, `QueryResponse`) to wire frames. Transport-
6//! shaped variants (`BatchEnd`, `EngineError`) are handled by the
7//! runtime directly and never reach this encoder.
8
9use melin_app::encoder::ResponseEncoder as ResponseEncoderTrait;
10use melin_protocol::codec;
11use melin_protocol::message::ResponseKind;
12use melin_types::types::{ExecutionReport, QueryResponse};
13
14/// Encoder for the trading wire protocol.
15///
16/// Zero-sized. The runtime owns an `Arc<dyn ResponseEncoder<...>>`;
17/// constructing one is `Arc::new(ResponseEncoder)`.
18#[derive(Debug, Clone, Copy)]
19pub struct ResponseEncoder;
20
21impl ResponseEncoderTrait for ResponseEncoder {
22    type Report = ExecutionReport;
23    type Query = QueryResponse;
24
25    fn encode_report(
26        &self,
27        report: &ExecutionReport,
28        buf: &mut [u8],
29    ) -> Result<usize, &'static str> {
30        codec::encode_response(&ResponseKind::Report(*report), buf).map_err(|_| "encode error")
31    }
32
33    fn encode_query(&self, query: &QueryResponse, buf: &mut [u8]) -> Result<usize, &'static str> {
34        let kind = match *query {
35            QueryResponse::Stats {
36                active_connections,
37                events_processed,
38                journal_sequence,
39            } => ResponseKind::StatsHeader {
40                active_connections,
41                events_processed,
42                journal_sequence,
43            },
44            QueryResponse::Position {
45                account,
46                balances,
47                count,
48            } => ResponseKind::PositionSnapshot {
49                account,
50                balances,
51                count,
52            },
53            QueryResponse::RequestSeqHwm { hwm } => ResponseKind::RequestSeqHwm { hwm },
54        };
55        codec::encode_response(&kind, buf).map_err(|_| "encode error")
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use std::num::NonZeroU64;
63
64    use melin_types::types::*;
65
66    const SCRATCH: usize = 512;
67
68    /// Strip the 4-byte length prefix `encode_response` writes and
69    /// hand the payload to `decode_response`. Keeps the round-trip
70    /// asserts below symmetric.
71    fn round_trip(written: &[u8]) -> ResponseKind {
72        codec::decode_response(&written[4..]).expect("decode")
73    }
74
75    fn sample_placed() -> ExecutionReport {
76        ExecutionReport::Placed {
77            order_id: OrderId(1),
78            symbol: Symbol(1),
79            account: AccountId(1),
80            side: Side::Buy,
81            price: Price(NonZeroU64::new(100).unwrap()),
82            quantity: Quantity(NonZeroU64::new(10).unwrap()),
83        }
84    }
85
86    #[test]
87    fn encodes_report() {
88        let mut buf = [0u8; SCRATCH];
89        let n = ResponseEncoder
90            .encode_report(&sample_placed(), &mut buf)
91            .unwrap();
92        assert!(matches!(
93            round_trip(&buf[..n]),
94            ResponseKind::Report(ExecutionReport::Placed { order_id, .. })
95                if order_id == OrderId(1)
96        ));
97    }
98
99    #[test]
100    fn encodes_query_stats() {
101        let q = QueryResponse::Stats {
102            active_connections: 7,
103            events_processed: 12345,
104            journal_sequence: 999,
105        };
106        let mut buf = [0u8; SCRATCH];
107        let n = ResponseEncoder.encode_query(&q, &mut buf).unwrap();
108        assert!(matches!(
109            round_trip(&buf[..n]),
110            ResponseKind::StatsHeader {
111                active_connections: 7,
112                events_processed: 12345,
113                journal_sequence: 999,
114            }
115        ));
116    }
117
118    #[test]
119    fn encodes_query_position() {
120        let mut balances = [AccountBalance::ZERO; 16];
121        balances[0] = AccountBalance {
122            currency: CurrencyId(1),
123            free: 100,
124            reserved: 0,
125        };
126        let q = QueryResponse::Position {
127            account: AccountId(42),
128            balances,
129            count: 1,
130        };
131        let mut buf = [0u8; SCRATCH];
132        let n = ResponseEncoder.encode_query(&q, &mut buf).unwrap();
133        assert!(matches!(
134            round_trip(&buf[..n]),
135            ResponseKind::PositionSnapshot { account, count: 1, .. }
136                if account == AccountId(42)
137        ));
138    }
139
140    #[test]
141    fn encodes_query_request_seq_hwm() {
142        let q = QueryResponse::RequestSeqHwm { hwm: 4242 };
143        let mut buf = [0u8; SCRATCH];
144        let n = ResponseEncoder.encode_query(&q, &mut buf).unwrap();
145        assert!(matches!(
146            round_trip(&buf[..n]),
147            ResponseKind::RequestSeqHwm { hwm: 4242 }
148        ));
149    }
150
151    // Note: the encoder's `Err` arm exists for codec-level failures
152    // (e.g. an `InvalidField` propagated up); the codec does NOT
153    // check buffer length and will panic with index-out-of-bounds
154    // on an undersized scratch. The runtime always passes
155    // `MAX_RESPONSE_BUF` (8 KiB), which is sized to fit any single
156    // wire response, so this is a caller-guarantee contract — not
157    // something the encoder defends against.
158}