Skip to main content

HeaderColumn

HeaderColumn lets a headers column hold a type of the service's own; it moves the map in and out and names a header it cannot keep byte for byte.

The trait has three methods. take_headers moves the headers the column holds into a header map, once per delivery as its row is claimed, so a column that owns its strings hands them over without a copy. from_headers builds the column value that holds a message's headers, for the service's Publish. unfit names the first header the column cannot hold byte for byte, and the broker refuses a publish that carries it with SqlxBrokerError::Header.

The crate implements it for Json<BTreeMap<String, String>> under the json feature, and for Option<T> of any implementation, where None holds no headers. The trait is shared by the inbox and the outbox: one column type serves a queue table and an outbox table alike. The type is also a sqlx type of its own, so the driver reads and writes the column through it.

Example​

Lines keeps the headers one per line, name: value, in a TEXT column, and refuses a value that is not text on one line.

use ruststream::HeaderMap;
use ruststream_sqlx::HeaderColumn;

/// Headers kept one per line, `name: value`, in a `TEXT` column.
#[derive(sqlx::Type)]
#[sqlx(transparent)]
struct Lines(String);

impl HeaderColumn for Lines {
fn take_headers(&mut self) -> HeaderMap {
let headers = self
.0
.lines()
.filter_map(|line| line.split_once(": "))
.map(|(name, value)| (name.to_owned(), value.to_owned()))
.collect();
self.0.clear();
headers
}

fn from_headers(headers: &HeaderMap) -> Self {
let lines: Vec<String> = headers
.iter()
.map(|(name, value)| format!("{name}: {}", String::from_utf8_lossy(value)))
.collect();
Self(lines.join("\n"))
}

fn unfit(headers: &HeaderMap) -> Option<&str> {
// A value comes back as it went in when it is text on one line.
let fits = |value: &[u8]| str::from_utf8(value).is_ok_and(|text| !text.contains('\n'));
headers
.iter()
.find(|(_, value)| !fits(value))
.map(|(name, _)| name)
}
}

#[derive(Inbox, sqlx::FromRow)]
#[inbox(table = "export_jobs")]
struct ExportJob {
#[field(id, generated)]
id: i64,
#[field(processed_at)]
processed_at: Option<DateTime<Utc>>,
#[field(locked_until)]
locked_until: Option<DateTime<Utc>>,
#[field(headers)]
meta: Lines,
#[field(payload)]
payload: Vec<u8>,
}

impl Publish<Sqlite> for ExportJob {
async fn publish(
conn: &mut SqliteConnection,
message: &OutgoingMessage<'_>,
) -> Result<(), sqlx::Error> {
// The column keeps the message's headers as the delivery reads them back.
let headers = Lines::from_headers(message.headers());
sqlx::query("INSERT INTO export_jobs (meta, payload) VALUES (?, ?)")
.bind(headers)
.bind(message.payload())
.execute(conn)
.await?;
Ok(())
}
}