use std::str::{FromStr, from_utf8};
use async_nats::{HeaderName, HeaderValue};
use bytes::{Bytes, BytesMut};
use ruststream::{HeaderMap, OutgoingMessage};
use crate::error::NatsError;
pub(crate) fn headers_from_nats(map: Option<&async_nats::HeaderMap>) -> HeaderMap {
let Some(map) = map else {
return HeaderMap::new();
};
let mut headers = HeaderMap::new();
for (name, values) in map.iter() {
if let Some(first) = values.iter().next() {
headers.insert(name.to_string(), Bytes::copy_from_slice(first.as_ref()));
}
}
headers
}
pub(crate) fn nats_parts(
msg: OutgoingMessage<'_, BytesMut>,
) -> Result<(String, Bytes, Option<async_nats::HeaderMap>), NatsError> {
let (subject, payload, headers) = msg.into_parts();
let headers = headers_to_nats(&headers)?;
Ok((subject.to_owned(), payload.freeze(), headers))
}
pub(crate) fn headers_to_nats(
headers: &HeaderMap,
) -> Result<Option<async_nats::HeaderMap>, NatsError> {
if headers.is_empty() {
return Ok(None);
}
let mut map = async_nats::HeaderMap::new();
for (name, value) in headers.iter() {
let header = HeaderName::from_str(name).map_err(|_| {
rejected(
name,
"a NATS header name is printable ASCII and carries no colon",
)
})?;
let text = from_utf8(value).map_err(|_| {
rejected(
name,
"a NATS header value is text, and this one is not UTF-8",
)
})?;
let value = HeaderValue::from_str(text).map_err(|_| {
rejected(
name,
"a NATS header value is one line, and this one contains a newline",
)
})?;
map.insert(header, value);
}
Ok(Some(map))
}
fn rejected(name: &str, why: &str) -> NatsError {
NatsError::Publish(format!("header `{name}` cannot be sent over NATS: {why}").into())
}
#[cfg(test)]
mod tests {
use super::*;
fn one(name: &str, value: impl Into<Bytes>) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(name.to_owned(), value.into());
headers
}
#[test]
fn the_client_is_handed_the_buffer_the_framework_wrote() {
let payload = BytesMut::from(&br#"{"id":1}"#[..]);
let written_at = payload.as_ptr();
let (subject, body, headers) =
nats_parts(OutgoingMessage::produced("orders.created", payload))
.expect("a message with no headers has a NATS form");
assert_eq!(subject, "orders.created");
assert!(headers.is_none());
assert_eq!(
body.as_ptr(),
written_at,
"async-nats keeps the payload, so the buffer travels into the client rather than \
being copied into a second one",
);
}
#[test]
fn a_text_header_survives_the_conversion() {
let map = headers_to_nats(&one("x-tenant", "acme"))
.expect("plain text is what NATS headers carry")
.expect("a non-empty map converts to a non-empty map");
assert_eq!(map.get("x-tenant").map(HeaderValue::as_str), Some("acme"));
}
#[test]
fn an_empty_map_has_no_nats_form() {
assert!(
headers_to_nats(&HeaderMap::new())
.expect("an empty map is not an error")
.is_none()
);
}
#[test]
fn a_header_the_protocol_cannot_carry_fails_the_publish() {
for (name, value) in [
("x-note", b"line one\r\nline two".as_slice()),
("x:note", b"fine"),
("x-key", &[0xff, 0x01]),
] {
let err = headers_to_nats(&one(name, Bytes::copy_from_slice(value)))
.expect_err("this header has no NATS form");
let message = err.to_string();
assert!(
message.contains(name),
"the error must name the header it is about, got: {message}",
);
}
}
}