1use std::fmt;
9
10use bytes::Bytes;
11use fe2o3_amqp::link::delivery::DeliveryInfo;
12use fe2o3_amqp_types::messaging::{
13 ApplicationProperties, Body, Data, Message, MessageId, Properties,
14};
15use fe2o3_amqp_types::primitives::{Binary, SimpleValue, Symbol, Value};
16use ruststream::{AckError, HeaderMap, IncomingMessage, OutgoingFor, Partitioned, Str, Take};
17use tokio::sync::{mpsc, oneshot};
18
19use crate::error::AmqpError;
20#[cfg(feature = "testing")]
21use crate::in_process::{Delivery, Settlement};
22
23pub const PARTITION_KEY_HEADER: &str = "partition-key";
28
29#[derive(Debug, Clone, Copy)]
31pub(crate) enum SettleKind {
32 Accept,
34 Modify,
42 Reject,
45}
46
47#[derive(Debug)]
49pub(crate) struct SettleCmd {
50 pub(crate) info: DeliveryInfo,
51 pub(crate) kind: SettleKind,
52 pub(crate) done: oneshot::Sender<Result<(), AckError>>,
53}
54
55pub(crate) type SettleSender = mpsc::UnboundedSender<SettleCmd>;
56
57pub struct AmqpMessage {
65 payload: Bytes,
66 headers: HeaderMap,
67 delivery_count: Option<u32>,
70 settle: Option<SettleHandle>,
72 #[cfg(feature = "testing")]
75 in_process: Option<Settlement>,
76}
77
78struct SettleHandle {
79 tx: SettleSender,
80 info: DeliveryInfo,
81}
82
83impl fmt::Debug for AmqpMessage {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 f.debug_struct("AmqpMessage")
86 .field("payload_len", &self.payload.len())
87 .field("delivery_count", &self.delivery_count)
88 .field("settled", &self.settle.is_none())
89 .finish_non_exhaustive()
90 }
91}
92
93impl AmqpMessage {
94 pub(crate) fn unsettled(
95 payload: Bytes,
96 headers: HeaderMap,
97 delivery_count: Option<u32>,
98 tx: SettleSender,
99 info: DeliveryInfo,
100 ) -> Self {
101 Self {
102 payload,
103 headers,
104 delivery_count,
105 settle: Some(SettleHandle { tx, info }),
106 #[cfg(feature = "testing")]
107 in_process: None,
108 }
109 }
110
111 pub(crate) fn settled(payload: Bytes, headers: HeaderMap, delivery_count: Option<u32>) -> Self {
114 Self {
115 payload,
116 headers,
117 delivery_count,
118 settle: None,
119 #[cfg(feature = "testing")]
120 in_process: None,
121 }
122 }
123
124 #[cfg(feature = "testing")]
126 pub(crate) fn in_process(delivery: Delivery, settlement: Settlement) -> Self {
127 Self::settled(delivery.payload, delivery.headers, delivery.count).settling(settlement)
128 }
129
130 #[cfg(feature = "testing")]
132 pub(crate) fn settling(mut self, settlement: Settlement) -> Self {
133 self.in_process = Some(settlement);
134 self
135 }
136
137 async fn settle(self, kind: SettleKind) -> Result<(), AckError> {
138 #[cfg(feature = "testing")]
139 if let Some(settlement) = self.in_process {
140 return settlement.settle(kind, self.payload, self.headers, self.delivery_count);
141 }
142 let Some(SettleHandle { tx, info }) = self.settle else {
143 return Err(AckError::Unsupported);
144 };
145 let (done, wait) = oneshot::channel();
146 tx.send(SettleCmd { info, kind, done }).map_err(|_| {
147 AckError::Broker(Box::from("the subscription's pump task has shut down"))
148 })?;
149 wait.await.map_err(|_| {
150 AckError::Broker(Box::from("the subscription's pump task has shut down"))
151 })?
152 }
153}
154
155impl Partitioned for AmqpMessage {
156 fn partition_key(&self) -> Option<&[u8]> {
157 self.headers.get(PARTITION_KEY_HEADER)
158 }
159}
160
161impl IncomingMessage for AmqpMessage {
162 fn payload(&self) -> &[u8] {
163 &self.payload
164 }
165
166 fn headers(&self) -> &HeaderMap {
167 &self.headers
168 }
169
170 async fn ack(self) -> Result<(), AckError> {
171 self.settle(SettleKind::Accept).await
172 }
173
174 async fn nack(self, requeue: bool) -> Result<(), AckError> {
175 let kind = if requeue {
176 SettleKind::Modify
177 } else {
178 SettleKind::Reject
179 };
180 self.settle(kind).await
181 }
182
183 fn redelivery_count(&self) -> Option<u64> {
192 self.delivery_count.map(|count| u64::from(count) + 1)
193 }
194
195 fn partition_key(&self) -> Option<&[u8]> {
196 Partitioned::partition_key(self)
197 }
198}
199
200pub(crate) fn to_amqp_message(msg: OutgoingFor<'_, Take>) -> Message<Data> {
202 let (_, payload, headers) = msg.into_parts();
203 build_message(&headers, Vec::from(payload))
204}
205
206pub(crate) fn build_message(headers: &HeaderMap, body: Vec<u8>) -> Message<Data> {
209 let mut properties = Properties::default();
210 let mut has_properties = false;
211 let mut application: Option<ApplicationProperties> = None;
212
213 for (name, value) in headers.iter() {
214 let text = || String::from_utf8_lossy(value).into_owned();
215 match name {
216 "content-type" => {
217 properties.content_type = Some(Symbol::from(text()));
218 has_properties = true;
219 }
220 "correlation-id" => {
221 properties.correlation_id = Some(MessageId::String(text()));
222 has_properties = true;
223 }
224 "reply-to" => {
225 properties.reply_to = Some(text());
226 has_properties = true;
227 }
228 "message-id" => {
229 properties.message_id = Some(MessageId::String(text()));
230 has_properties = true;
231 }
232 PARTITION_KEY_HEADER => {
233 properties.group_id = Some(text());
234 has_properties = true;
235 }
236 other => {
237 let simple = std::str::from_utf8(value).map_or_else(
238 |_| SimpleValue::Binary(Binary::from(value.to_vec())),
239 |s| SimpleValue::String(s.to_owned()),
240 );
241 application
242 .get_or_insert_with(ApplicationProperties::default)
243 .insert(other.to_owned(), simple);
244 }
245 }
246 }
247
248 let mut builder = Message::builder();
249 if has_properties {
250 builder = builder.properties(properties);
251 }
252 if let Some(application) = application {
253 builder = builder.application_properties(application);
254 }
255 builder.data(Binary::from(body)).build()
256}
257
258pub(crate) fn headers_from_amqp<B>(message: &Message<B>) -> HeaderMap {
260 let mut headers = HeaderMap::new();
261 if let Some(properties) = &message.properties {
262 if let Some(content_type) = &properties.content_type {
265 headers.insert(Str::from_static("content-type"), content_type.to_string());
266 }
267 if let Some(correlation_id) = &properties.correlation_id {
268 headers.insert(
269 Str::from_static("correlation-id"),
270 message_id_text(correlation_id),
271 );
272 }
273 if let Some(reply_to) = &properties.reply_to {
274 headers.insert(Str::from_static("reply-to"), reply_to.clone());
275 }
276 if let Some(message_id) = &properties.message_id {
277 headers.insert(Str::from_static("message-id"), message_id_text(message_id));
278 }
279 if let Some(group_id) = &properties.group_id {
280 headers.insert(Str::from_static(PARTITION_KEY_HEADER), group_id.clone());
281 }
282 }
283 if let Some(application) = &message.application_properties {
284 for (name, value) in application.iter() {
285 headers.insert(name.clone(), simple_value_bytes(value));
286 }
287 }
288 headers
289}
290
291fn message_id_text(id: &MessageId) -> String {
293 match id {
294 MessageId::String(s) => s.clone(),
295 MessageId::Uuid(u) => format!("{u:x}"),
296 MessageId::Ulong(n) => n.to_string(),
297 MessageId::Binary(b) => String::from_utf8_lossy(b).into_owned(),
298 }
299}
300
301fn simple_value_bytes(value: &SimpleValue) -> Bytes {
302 match value {
303 SimpleValue::String(s) => Bytes::copy_from_slice(s.as_bytes()),
304 SimpleValue::Binary(b) => Bytes::copy_from_slice(b),
305 SimpleValue::Symbol(s) => Bytes::copy_from_slice(s.as_str().as_bytes()),
306 other => Bytes::from(format_simple_value(other)),
307 }
308}
309
310fn format_simple_value(value: &SimpleValue) -> String {
313 match value {
314 SimpleValue::Bool(v) => v.to_string(),
315 SimpleValue::Ubyte(v) => v.to_string(),
316 SimpleValue::Ushort(v) => v.to_string(),
317 SimpleValue::Uint(v) => v.to_string(),
318 SimpleValue::Ulong(v) => v.to_string(),
319 SimpleValue::Byte(v) => v.to_string(),
320 SimpleValue::Short(v) => v.to_string(),
321 SimpleValue::Int(v) => v.to_string(),
322 SimpleValue::Long(v) => v.to_string(),
323 SimpleValue::Float(v) => v.to_string(),
324 SimpleValue::Double(v) => v.to_string(),
325 SimpleValue::Char(v) => v.to_string(),
326 SimpleValue::Timestamp(v) => v.milliseconds().to_string(),
327 SimpleValue::Uuid(v) => format!("{v:x}"),
328 other => format!("{other:?}"),
329 }
330}
331
332pub(crate) fn payload_from_body(body: Body<Value>, address: &str) -> Result<Bytes, AmqpError> {
338 match body {
339 Body::Data(batch) => {
340 let mut chunks = batch.into_iter();
341 match (chunks.next(), chunks.next()) {
342 (None, _) => Ok(Bytes::new()),
343 (Some(Data(first)), None) => Ok(Bytes::from(first.into_vec())),
344 (Some(Data(first)), Some(Data(second))) => {
345 let mut all = first.into_vec();
346 all.extend_from_slice(&second);
347 for Data(chunk) in chunks {
348 all.extend_from_slice(&chunk);
349 }
350 Ok(Bytes::from(all))
351 }
352 }
353 }
354 Body::Value(value) => match value.0 {
355 Value::Binary(b) => Ok(Bytes::from(b.into_vec())),
356 Value::String(s) => Ok(Bytes::from(s.into_bytes())),
357 _ => Err(AmqpError::UnsupportedBody {
358 address: address.to_owned(),
359 }),
360 },
361 Body::Empty => Ok(Bytes::new()),
362 Body::Sequence(_) => Err(AmqpError::UnsupportedBody {
363 address: address.to_owned(),
364 }),
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use ruststream::{BytesMut, OutgoingMessage};
371
372 use super::*;
373
374 #[test]
377 fn the_data_body_keeps_the_buffer_the_framework_wrote() {
378 let payload = BytesMut::from(&br#"{"id":1}"#[..]);
379 let written = payload.as_ptr();
380 let outgoing: OutgoingFor<'_, Take> = OutgoingMessage::produced("orders", payload);
381
382 let message = to_amqp_message(outgoing);
383
384 assert_eq!(
385 message.body.0.as_ptr(),
386 written,
387 "the client keeps the body until the transfer settles, so the buffer is handed over \
388 rather than copied"
389 );
390 }
391
392 #[test]
393 fn well_known_headers_ride_the_properties_section() {
394 let mut headers = HeaderMap::new();
395 headers.insert("content-type", "application/json");
396 headers.insert("correlation-id", "corr-1");
397 headers.insert("reply-to", "replies");
398 headers.insert("message-id", "msg-1");
399 headers.insert(PARTITION_KEY_HEADER, "user-42");
400 headers.insert("x-custom", "value");
401 let outgoing = OutgoingMessage::new("orders", b"{}".as_slice()).with_headers(headers);
402
403 let message = to_amqp_message(outgoing);
404 let properties = message.properties.as_ref().expect("properties set");
405 assert_eq!(
406 properties.content_type.as_ref().map(Symbol::as_str),
407 Some("application/json")
408 );
409 assert_eq!(
410 properties.correlation_id,
411 Some(MessageId::String("corr-1".into()))
412 );
413 assert_eq!(properties.reply_to.as_deref(), Some("replies"));
414 assert_eq!(properties.group_id.as_deref(), Some("user-42"));
415 let application = message
416 .application_properties
417 .as_ref()
418 .expect("application properties set");
419 assert_eq!(
420 application.get("x-custom"),
421 Some(&SimpleValue::String("value".into()))
422 );
423 assert!(application.get("content-type").is_none());
424 }
425
426 #[test]
427 fn headers_round_trip_through_the_amqp_sections() {
428 let mut headers = HeaderMap::new();
429 headers.insert("content-type", "application/json");
430 headers.insert("correlation-id", "corr-1");
431 headers.insert(PARTITION_KEY_HEADER, "user-42");
432 headers.insert("x-custom", "value");
433 let outgoing =
434 OutgoingMessage::new("orders", b"{}".as_slice()).with_headers(headers.clone());
435
436 let restored = headers_from_amqp(&to_amqp_message(outgoing));
437 assert_eq!(restored.get_str("content-type"), Some("application/json"));
438 assert_eq!(restored.get_str("correlation-id"), Some("corr-1"));
439 assert_eq!(restored.get_str(PARTITION_KEY_HEADER), Some("user-42"));
440 assert_eq!(restored.get_str("x-custom"), Some("value"));
441 }
442
443 #[test]
444 fn data_body_yields_payload_bytes() {
445 let outgoing = OutgoingMessage::new("orders", b"payload".as_slice());
446 let message = to_amqp_message(outgoing);
447 let body = Body::<Value>::Data(vec![message.body].into());
448 let payload = payload_from_body(body, "orders").expect("data body decodes");
449 assert_eq!(payload.as_ref(), b"payload");
450 }
451
452 #[test]
453 fn string_and_binary_values_are_accepted_as_bytes() {
454 let s = Body::Value(fe2o3_amqp_types::messaging::AmqpValue(Value::String(
455 "hi".into(),
456 )));
457 assert_eq!(
458 payload_from_body(s, "a").expect("string decodes").as_ref(),
459 b"hi"
460 );
461
462 let b = Body::Value(fe2o3_amqp_types::messaging::AmqpValue(Value::Binary(
463 Binary::from(b"raw".to_vec()),
464 )));
465 assert_eq!(
466 payload_from_body(b, "a").expect("binary decodes").as_ref(),
467 b"raw"
468 );
469 }
470
471 #[test]
472 fn foreign_value_bodies_are_reported_unsupported() {
473 let body = Body::Value(fe2o3_amqp_types::messaging::AmqpValue(Value::Bool(true)));
474 assert!(matches!(
475 payload_from_body(body, "a"),
476 Err(AmqpError::UnsupportedBody { .. })
477 ));
478 }
479}