microsandbox_agent_client/message.rs
1//! Outbound agent message builders.
2//!
3//! These types represent the semantic message before the client assigns a
4//! correlation ID and writes a transport packet.
5
6use microsandbox_protocol::codec::RawFrame;
7use microsandbox_protocol::message::{Message, MessageType};
8use serde::Serialize;
9
10use crate::{AgentClientError, AgentClientResult};
11
12//--------------------------------------------------------------------------------------------------
13// Types
14//--------------------------------------------------------------------------------------------------
15
16/// A message described by protocol type and a native serializable payload.
17///
18/// Use this form when Rust should CBOR-encode the payload. The payload type must
19/// serialize to the shape expected by `microsandbox-protocol` for
20/// [`message_type`](Self::message_type).
21#[derive(Debug, Clone)]
22pub struct TypedMessage<T> {
23 /// Protocol message type.
24 pub message_type: MessageType,
25 /// Native payload to CBOR-encode.
26 pub payload: T,
27}
28
29/// A message described by protocol type and already-CBOR-encoded payload bytes.
30///
31/// The payload is only the message-type payload, not the outer protocol
32/// envelope and not the length-prefixed transport packet. The client still
33/// builds `{ v, t, p }`, derives frame flags, and assigns the correlation ID.
34#[derive(Debug, Clone)]
35pub struct EncodedMessage {
36 /// Protocol message type.
37 pub message_type: MessageType,
38 /// CBOR-encoded payload bytes.
39 pub payload: Vec<u8>,
40}
41
42/// Fully encoded outbound message body, before correlation ID assignment.
43///
44/// This is the CBOR protocol envelope body that belongs after the binary frame
45/// header. It is mostly useful for transport adapters, tests, and language
46/// bindings.
47#[derive(Debug, Clone)]
48pub struct OutboundMessage {
49 /// Protocol message type.
50 pub message_type: MessageType,
51 /// Frame flags derived from the message type.
52 pub flags: u8,
53 /// CBOR-encoded protocol envelope body.
54 pub body: Vec<u8>,
55}
56
57/// Converts a typed or encoded message into an outbound protocol body.
58///
59/// Implementations must fail before sending when the negotiated peer generation
60/// is too old for the selected message type.
61pub trait IntoOutboundMessage {
62 /// Encode the outbound message for a connection.
63 ///
64 /// `protocol_version` is the generation written into the envelope.
65 /// `negotiated_version` is the capability gate used before encoding.
66 fn into_outbound_message(
67 self,
68 protocol_version: u8,
69 negotiated_version: u8,
70 ) -> AgentClientResult<OutboundMessage>;
71}
72
73//--------------------------------------------------------------------------------------------------
74// Methods
75//--------------------------------------------------------------------------------------------------
76
77impl<T> TypedMessage<T> {
78 /// Create a typed message from a protocol type and native payload.
79 pub fn new(message_type: MessageType, payload: T) -> Self {
80 Self {
81 message_type,
82 payload,
83 }
84 }
85}
86
87impl EncodedMessage {
88 /// Create an encoded message from a protocol type and CBOR payload bytes.
89 pub fn new(message_type: MessageType, payload: impl Into<Vec<u8>>) -> Self {
90 Self {
91 message_type,
92 payload: payload.into(),
93 }
94 }
95}
96
97impl OutboundMessage {
98 /// Convert the outbound message into a raw frame with an assigned ID.
99 pub fn into_raw_frame(self, id: u32) -> RawFrame {
100 RawFrame {
101 id,
102 flags: self.flags,
103 body: self.body,
104 }
105 }
106}
107
108//--------------------------------------------------------------------------------------------------
109// Trait Implementations
110//--------------------------------------------------------------------------------------------------
111
112impl<T> IntoOutboundMessage for TypedMessage<T>
113where
114 T: Serialize,
115{
116 fn into_outbound_message(
117 self,
118 protocol_version: u8,
119 negotiated_version: u8,
120 ) -> AgentClientResult<OutboundMessage> {
121 let mut payload = Vec::new();
122 ciborium::into_writer(&self.payload, &mut payload)
123 .map_err(|error| AgentClientError::Cbor(error.to_string()))?;
124 EncodedMessage::new(self.message_type, payload)
125 .into_outbound_message(protocol_version, negotiated_version)
126 }
127}
128
129impl IntoOutboundMessage for EncodedMessage {
130 fn into_outbound_message(
131 self,
132 protocol_version: u8,
133 negotiated_version: u8,
134 ) -> AgentClientResult<OutboundMessage> {
135 if !self.message_type.is_available_at(negotiated_version) {
136 return Err(AgentClientError::UnsupportedOperation {
137 msg_type: self.message_type.as_str(),
138 needs: self.message_type.min_protocol_version(),
139 peer: negotiated_version,
140 });
141 }
142
143 let flags = self.message_type.flags();
144 let message = Message {
145 v: protocol_version,
146 t: self.message_type,
147 id: 0,
148 flags,
149 p: self.payload,
150 };
151 let mut body = Vec::new();
152 ciborium::into_writer(&message, &mut body)
153 .map_err(|error| AgentClientError::Cbor(error.to_string()))?;
154
155 Ok(OutboundMessage {
156 message_type: self.message_type,
157 flags,
158 body,
159 })
160 }
161}