ruststream_sqlx/inbox/headers.rs
1//! The headers layout: a headers struct describes the queue table, and the message struct a
2//! handler takes flattens it into its `#[field(headers)]` field beside data of its own. The
3//! delivery's header map holds the headers struct's fields without a role, built on the first
4//! `headers()` call.
5
6use std::fmt::Debug;
7use std::io::Write;
8#[cfg(any(feature = "chrono", feature = "time"))]
9use std::io::{self, Cursor};
10use std::sync::OnceLock;
11
12#[cfg(feature = "chrono")]
13use chrono::format::{Fixed, Item};
14#[cfg(feature = "chrono")]
15use chrono::{DateTime, Utc};
16use ruststream::{HeaderMap, Str};
17#[cfg(feature = "time")]
18use time::OffsetDateTime;
19#[cfg(feature = "time")]
20use time::format_description::well_known::Rfc3339;
21
22use super::InboxSpec;
23use super::database::QueueDatabase;
24use super::engine::Events;
25use super::spec::Declaration;
26
27/// A struct that describes a queue table whose message is assembled from it: the mechanics a
28/// subscription runs the queue by, and the service's own headers. `#[derive(InboxHeaders)]`
29/// implements it.
30///
31/// The headers struct takes the table's attributes (`#[inbox(table, schema, advisory_lock,
32/// clock, isolation, mode)]`) and the fields that play a role. Every field without a role is a
33/// header: the delivery's header map holds it under its column's name, built the first time
34/// something reads the delivery's headers, and a field that holds `None` is left out. Its type
35/// implements [`HeaderField`]. The derive also writes the generated
36/// [`insert`](crate::Insert::insert) of the table's columns.
37///
38/// The message struct derives [`Inbox`](crate::Inbox) and holds the headers struct in a
39/// `#[field(headers)] #[sqlx(flatten)]` field beside its own data. A handler takes the message
40/// struct itself, `&Message`, or `&[Message]` in a batch, as in row mode. Its rows are read
41/// by the default fetch, which names the headers struct's columns and the message's own, or by
42/// the message's own [`Fetch`](crate::Fetch) with `#[inbox(custom(fetch))]`, which may read the
43/// data from other tables.
44///
45/// # Examples
46///
47/// ```
48/// # #[cfg(feature = "postgres")]
49/// # mod demo {
50/// use ruststream_sqlx::prelude::*;
51/// use serde::Deserialize;
52/// use sqlx::PgPool;
53///
54/// /// The queue table: the mechanics, and the service's own headers `tenant` and `trace`.
55/// #[derive(Debug, Clone, InboxHeaders, sqlx::FromRow)]
56/// #[inbox(table = "order_jobs")]
57/// pub struct OrderHeaders {
58/// #[field(id, generated)]
59/// job_id: i64,
60/// #[field(group)]
61/// name: String,
62/// #[field(attempt, generated)]
63/// attempt: i16,
64/// tenant: String,
65/// trace: Option<String>,
66/// }
67///
68/// /// The message a handler takes: the headers and the order's note, read from the same row.
69/// #[derive(Debug, Clone, Inbox, sqlx::FromRow)]
70/// pub struct OrderJob {
71/// #[field(headers)]
72/// #[sqlx(flatten)]
73/// headers: OrderHeaders,
74/// note: Option<String>,
75/// }
76///
77/// #[derive(Deserialize)]
78/// struct Tenant {
79/// tenant: String,
80/// }
81///
82/// // The header map is built from `OrderHeaders` on the first read: here, by `Headers<Tenant>`.
83/// #[subscriber(InboxQueue::<OrderJob>::new("orders"))]
84/// async fn ship(job: &OrderJob, Headers(tenant): Headers<Tenant>) -> HandlerOutcome {
85/// tracing::info!(tenant = %tenant.tenant, note = ?job.note, "shipping");
86/// HandlerOutcome::ack()
87/// }
88///
89/// pub fn app(pool: PgPool) -> RustStream {
90/// RustStream::new(AppInfo::new("shop", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
91/// b.include(ship);
92/// })
93/// }
94/// # }
95/// # fn main() {}
96/// ```
97#[diagnostic::on_unimplemented(
98 message = "`{Self}` is not a headers struct: a flattened `#[field(headers)]` field holds one",
99 label = "this type does not derive `InboxHeaders`",
100 note = "derive `InboxHeaders` for `{Self}` and describe the queue table on it, or drop \
101 `#[sqlx(flatten)]` to read the field as a header column"
102)]
103pub trait InboxHeaders: Sized + Send + Sync + 'static {
104 /// The type of the field that plays `id`. Machinery; the derive sets it.
105 #[doc(hidden)]
106 type Id: Clone + Debug + Send + Sync + 'static;
107
108 /// The queue table's settings, as the markers of [`spec`](crate::spec): the mechanics and
109 /// the header fields. Machinery; the derive sets it.
110 #[doc(hidden)]
111 type Settings: Declaration;
112
113 /// The queue table: the mechanics and the headers, without the message's own columns, which
114 /// the message's own description adds. Machinery; the derive sets it.
115 #[doc(hidden)]
116 const TABLE: InboxSpec<Self::Settings>;
117
118 /// The headers the fields without a role hold, by their columns' names. Machinery; the
119 /// derive sets it.
120 #[doc(hidden)]
121 const NAMES: &'static [&'static str];
122
123 /// The row's id. Machinery; the derive writes it.
124 #[doc(hidden)]
125 fn id(&self) -> &Self::Id;
126
127 /// The header map of the fields without a role. Machinery; the derive writes it.
128 #[doc(hidden)]
129 fn header_map(&self) -> HeaderMap;
130}
131
132/// A row whose delivery builds its header map from its fields: where the map comes from.
133/// Machinery; every [`HeaderFields`](crate::HeaderFields) row implements it.
134#[doc(hidden)]
135pub trait Assembled {
136 /// The header map of the headers struct's fields without a role.
137 fn header_map(&self) -> HeaderMap;
138}
139
140/// A value a field of a headers struct holds, as the header the delivery's header map carries.
141///
142/// A field of a struct deriving [`InboxHeaders`] without a role becomes the header of its
143/// column's name. Strings and byte vectors are their bytes, integers and `bool` their decimal
144/// text, and `chrono::DateTime<Utc>` (feature `chrono`) and `time::OffsetDateTime` (feature
145/// `time`) their RFC 3339 text. `None` leaves the header out. A service implements it for a type
146/// of its own.
147///
148/// # Examples
149///
150/// ```
151/// # #[cfg(feature = "postgres")]
152/// # mod demo {
153/// use ruststream_sqlx::HeaderField;
154/// use ruststream_sqlx::prelude::*;
155///
156/// /// A region code the table keeps as text.
157/// #[derive(Debug, Clone, sqlx::Type)]
158/// #[sqlx(transparent)]
159/// pub struct Region(String);
160///
161/// // The header carries the code in capitals, as the service's consumers read it.
162/// impl HeaderField for Region {
163/// fn header(&self) -> Option<Vec<u8>> {
164/// Some(self.0.to_uppercase().into_bytes())
165/// }
166/// }
167///
168/// #[derive(Debug, Clone, InboxHeaders, sqlx::FromRow)]
169/// #[inbox(table = "shipments")]
170/// pub struct ShipmentHeaders {
171/// #[field(id, generated)]
172/// id: i64,
173/// region: Region,
174/// }
175///
176/// #[derive(Debug, Clone, Inbox, sqlx::FromRow)]
177/// pub struct Shipment {
178/// #[field(headers)]
179/// #[sqlx(flatten)]
180/// headers: ShipmentHeaders,
181/// address: String,
182/// }
183///
184/// #[subscriber(InboxQueue::<Shipment>::new("shipments"))]
185/// async fn ship(shipment: &Shipment, ctx: &mut Context<'_>) -> HandlerOutcome {
186/// let region = ctx.headers().get_str("region").unwrap_or("none");
187/// tracing::info!(region, to = %shipment.address, "shipping");
188/// HandlerOutcome::ack()
189/// }
190///
191/// pub fn app(pool: sqlx::PgPool) -> RustStream {
192/// RustStream::new(AppInfo::new("shipping", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
193/// b.include(ship);
194/// })
195/// }
196/// # }
197/// # fn main() {}
198/// ```
199#[diagnostic::on_unimplemented(
200 message = "`{Self}` is not a header value: a field of a headers struct without a role becomes \
201 a header",
202 label = "a header field of this type",
203 note = "implement `HeaderField` for `{Self}`, or give the field a role with `#[field(..)]`"
204)]
205pub trait HeaderField {
206 /// The header's value; `None` leaves the header out.
207 fn header(&self) -> Option<Vec<u8>>;
208}
209
210impl HeaderField for String {
211 fn header(&self) -> Option<Vec<u8>> {
212 Some(self.clone().into_bytes())
213 }
214}
215
216impl HeaderField for Vec<u8> {
217 fn header(&self) -> Option<Vec<u8>> {
218 Some(self.clone())
219 }
220}
221
222impl HeaderField for bool {
223 fn header(&self) -> Option<Vec<u8>> {
224 Some(if *self {
225 b"true".to_vec()
226 } else {
227 b"false".to_vec()
228 })
229 }
230}
231
232/// The longest decimal text of a 64-bit integer: `i64::MIN` and `u64::MAX` take 20 bytes.
233const DECIMAL_TEXT: usize = 20;
234
235/// `HeaderField` for integers: the decimal text.
236macro_rules! decimal {
237 ($($integer:ty),*) => {
238 $(
239 impl HeaderField for $integer {
240 fn header(&self) -> Option<Vec<u8>> {
241 // The text goes into a buffer of its own length: a `Vec` with spare capacity
242 // costs the map one more allocation as it turns into `Bytes`.
243 let mut text = [0_u8; DECIMAL_TEXT];
244 let mut rest = &mut text[..];
245 // The buffer holds the longest decimal of a 64-bit integer, so the write
246 // cannot run out of room.
247 write!(rest, "{self}").ok()?;
248 let written = DECIMAL_TEXT - rest.len();
249 Some(text[..written].to_vec())
250 }
251 }
252 )*
253 };
254}
255
256decimal!(i8, i16, i32, i64, u8, u16, u32, u64);
257
258impl<T: HeaderField> HeaderField for Option<T> {
259 fn header(&self) -> Option<Vec<u8>> {
260 self.as_ref().and_then(HeaderField::header)
261 }
262}
263
264/// The longest RFC 3339 text of a time: a six-digit year with its sign, nanoseconds and an offset.
265#[cfg(any(feature = "chrono", feature = "time"))]
266const TIME_TEXT: usize = 48;
267
268/// The text `write` puts into a buffer on the stack, copied into a `Vec` of its own length: a
269/// formatter's `String` keeps spare capacity, which costs the map one more allocation as the
270/// value turns into `Bytes`. `None` where the text does not fit or the formatter fails.
271#[cfg(any(feature = "chrono", feature = "time"))]
272fn exact_text(
273 write: impl FnOnce(&mut Cursor<[u8; TIME_TEXT]>) -> io::Result<()>,
274) -> Option<Vec<u8>> {
275 let mut text = Cursor::new([0_u8; TIME_TEXT]);
276 write(&mut text).ok()?;
277 let written = usize::try_from(text.position()).ok()?;
278 Some(text.get_ref()[..written].to_vec())
279}
280
281#[cfg(feature = "chrono")]
282impl HeaderField for DateTime<Utc> {
283 fn header(&self) -> Option<Vec<u8>> {
284 // The items `to_rfc3339` writes, into a buffer of the text's own length.
285 let rfc3339 = [Item::Fixed(Fixed::RFC3339)];
286 exact_text(|text| write!(text, "{}", self.format_with_items(rfc3339.iter())))
287 }
288}
289
290#[cfg(feature = "time")]
291impl HeaderField for OffsetDateTime {
292 fn header(&self) -> Option<Vec<u8>> {
293 exact_text(|text| {
294 self.format_into(text, &Rfc3339)
295 .map(drop)
296 .map_err(io::Error::other)
297 })
298 }
299}
300
301/// Puts the header `name` holding `field` into `headers`, unless `field` holds none. Machinery;
302/// the derive's header map calls it for each field without a role.
303#[doc(hidden)]
304pub(crate) fn put_header(headers: &mut HeaderMap, name: &'static str, field: &impl HeaderField) {
305 if let Some(value) = field.header() {
306 // A column's name is a constant: the map holds it as it is, where a `&str` copies.
307 headers.insert(Str::from_static(name), value);
308 }
309}
310
311/// The first of `headers` whose name is not among `names`: the headers a headers struct's fields
312/// hold. Machinery.
313#[must_use]
314#[doc(hidden)]
315pub(crate) fn unnamed_header<'h>(headers: &'h HeaderMap, names: &[&str]) -> Option<&'h str> {
316 headers
317 .iter()
318 .map(|(name, _)| name)
319 .find(|name| !names.iter().any(|known| known.eq_ignore_ascii_case(name)))
320}
321
322/// Where a delivery keeps its header map. Machinery: a flat struct's delivery holds the map it
323/// moved out of the row's headers column at the claim; a message assembled from a headers struct
324/// holds a cell the map is built into on the first read.
325#[doc(hidden)]
326pub trait HeaderCell<DB: QueueDatabase, Row: Events<DB>>: Default + Send + Sync + 'static {
327 /// The cell of `row`, as the claim hands the row over.
328 fn take(row: &mut Row) -> Self;
329
330 /// Whether the cell holds nothing yet: a batch keeps no room for such cells.
331 fn is_unset(&self) -> bool;
332
333 /// The header map; `row` is the delivery's row, `None` for an id without a row or a row that
334 /// did not decode.
335 fn read<'c>(&'c self, row: Option<&Row>) -> &'c HeaderMap;
336}
337
338impl<DB: QueueDatabase, Row: Events<DB>> HeaderCell<DB, Row> for HeaderMap {
339 fn take(row: &mut Row) -> Self {
340 Row::take_headers(row)
341 }
342
343 fn is_unset(&self) -> bool {
344 self.is_empty()
345 }
346
347 fn read<'c>(&'c self, _: Option<&Row>) -> &'c HeaderMap {
348 self
349 }
350}
351
352/// The header map of a message assembled from a headers struct, built on the first read.
353/// Machinery.
354// A `OnceLock`, not a `OnceCell`: a delivery is `Sync`. A read after the first is one atomic
355// load.
356#[doc(hidden)]
357#[derive(Debug, Default)]
358pub struct LazyHeaders(OnceLock<HeaderMap>);
359
360impl<DB, Row> HeaderCell<DB, Row> for LazyHeaders
361where
362 DB: QueueDatabase,
363 Row: Events<DB> + Assembled,
364{
365 fn take(_: &mut Row) -> Self {
366 Self::default()
367 }
368
369 fn is_unset(&self) -> bool {
370 self.0.get().is_none()
371 }
372
373 fn read<'c>(&'c self, row: Option<&Row>) -> &'c HeaderMap {
374 self.built(row)
375 }
376}
377
378impl LazyHeaders {
379 /// The header map of `row`, built on the first call.
380 fn built<Row: Assembled>(&self, row: Option<&Row>) -> &HeaderMap {
381 // A delivery without a row reads an empty map, which allocates nothing.
382 self.0
383 .get_or_init(|| row.map_or_else(HeaderMap::new, Assembled::header_map))
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 //! The lazy header map: built from the row on the first read, once.
390
391 use std::cell::Cell;
392
393 use ruststream::HeaderMap;
394
395 use super::{Assembled, HeaderField, LazyHeaders, put_header, unnamed_header};
396
397 /// A header value that counts how often it was read into a header.
398 struct Tenant {
399 name: &'static str,
400 reads: Cell<usize>,
401 }
402
403 impl HeaderField for Tenant {
404 fn header(&self) -> Option<Vec<u8>> {
405 self.reads.set(self.reads.get() + 1);
406 Some(self.name.as_bytes().to_vec())
407 }
408 }
409
410 /// A message whose header map holds its tenant, and its trace where it has one.
411 struct Job {
412 tenant: Tenant,
413 trace: Option<String>,
414 }
415
416 impl Assembled for Job {
417 fn header_map(&self) -> HeaderMap {
418 let mut headers = HeaderMap::new();
419 put_header(&mut headers, "tenant", &self.tenant);
420 put_header(&mut headers, "trace", &self.trace);
421 headers
422 }
423 }
424
425 fn job(trace: Option<&str>) -> Job {
426 Job {
427 tenant: Tenant {
428 name: "acme",
429 reads: Cell::new(0),
430 },
431 trace: trace.map(str::to_owned),
432 }
433 }
434
435 #[test]
436 fn the_map_is_built_on_the_first_read_and_only_once() {
437 let job = job(Some("t-1"));
438 let cell = LazyHeaders::default();
439 assert_eq!(job.tenant.reads.get(), 0, "nothing is built before a read");
440 let headers = cell.built(Some(&job));
441 assert_eq!(headers.get_str("tenant"), Some("acme"));
442 assert_eq!(headers.get_str("trace"), Some("t-1"));
443 assert_eq!(cell.built(Some(&job)).len(), 2);
444 assert_eq!(job.tenant.reads.get(), 1, "the second read reuses the map");
445 }
446
447 #[test]
448 fn a_field_holding_none_is_left_out_and_a_missing_row_reads_empty() {
449 let job = job(None);
450 let cell = LazyHeaders::default();
451 let headers = cell.built(Some(&job));
452 assert_eq!(headers.get_str("tenant"), Some("acme"));
453 assert!(!headers.contains("trace"), "a NULL trace is no header");
454 assert!(LazyHeaders::default().built(None::<&Job>).is_empty());
455 }
456
457 #[test]
458 fn a_header_no_field_is_named_for_is_unfit() {
459 let headers: HeaderMap = [("Tenant", "acme"), ("x-other", "1")].into_iter().collect();
460 assert_eq!(
461 unnamed_header(&headers, &["tenant", "trace"]),
462 Some("x-other")
463 );
464 assert_eq!(unnamed_header(&headers, &["tenant", "x-other"]), None);
465 }
466}