Skip to main content

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}