ruststream_sqlx/outbox/
publish.rs1use std::any::type_name;
5use std::error::Error as StdError;
6use std::fmt::{self, Display};
7use std::io::Write;
8use std::mem;
9use std::str::{self, FromStr};
10use std::sync::Arc;
11
12use ruststream::runtime::{Outgoing, PublishLayer, PublishNext, PublishPipeline};
13use ruststream::{Bytes, HeaderMap, OutgoingMessage, Publisher};
14use sqlx::Database;
15
16use super::OUTBOX_ID_HEADER;
17use super::error::OutboxError;
18use super::events::Publish;
19use super::registry::RecordList;
20use super::switch::enabled;
21use crate::outbox::store::Store;
22
23pub struct TrackingPublishLayer<DB: Database, Records> {
30 store: Arc<Store<DB>>,
31 records: Records,
32}
33
34impl<DB: Database, Records> TrackingPublishLayer<DB, Records> {
35 pub(super) const fn new(store: Arc<Store<DB>>, records: Records) -> Self {
36 Self { store, records }
37 }
38}
39
40impl<DB: Database, Records: Copy> Clone for TrackingPublishLayer<DB, Records> {
41 fn clone(&self) -> Self {
42 Self {
43 store: Arc::clone(&self.store),
44 records: self.records,
45 }
46 }
47}
48
49impl<DB: Database, Records: fmt::Debug> fmt::Debug for TrackingPublishLayer<DB, Records> {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 f.debug_struct("TrackingPublishLayer")
52 .field("records", &self.records)
53 .finish_non_exhaustive()
54 }
55}
56
57impl<DB: Database, Records: RecordList<DB>> PublishLayer for TrackingPublishLayer<DB, Records> {
58 async fn on_publish<'a, N: PublishPipeline, P: Publisher>(
59 &'a self,
60 out: &'a mut Outgoing<'a>,
61 next: PublishNext<'a, N, P>,
62 ) -> Result<(), Box<dyn StdError + Send + Sync>> {
63 if enabled() && self.records.contains(out.name()) {
64 let headers = mem::take(out.headers_mut());
67 let msg =
68 OutgoingMessage::with_payload(out.name(), out.payload()).with_headers(headers);
69 let recorded = self.records.record(&self.store, &msg).await;
70 let (_, _, headers) = msg.into_parts();
71 *out.headers_mut() = headers;
72 if let Some(id) = recorded {
73 out.headers_mut().insert(OUTBOX_ID_HEADER, id?);
74 }
75 }
76 next.run(out).await
77 }
78}
79
80pub(super) async fn record<DB: Database, Record: Publish<DB>>(
82 name: &'static str,
83 store: &Store<DB>,
84 msg: &OutgoingMessage<'_>,
85) -> Result<Bytes, OutboxError> {
86 let pool = store.get().ok_or(OutboxError::NoPool { name })?;
87 let failed = |source| OutboxError::Record {
88 name,
89 record: type_name::<Record>(),
90 source,
91 };
92 let mut conn = store.acquire(pool).await.map_err(failed)?;
93 let id = Record::publish(&mut conn, msg).await.map_err(failed)?;
94 Ok(id_value(&id))
95}
96
97const ID_ON_STACK: usize = 64;
99
100pub(super) fn id_value(id: &impl Display) -> Bytes {
105 let mut text = [0_u8; ID_ON_STACK];
106 let mut rest = &mut text[..];
107 if write!(rest, "{id}").is_ok() {
108 let len = ID_ON_STACK - rest.len();
109 return Bytes::copy_from_slice(&text[..len]);
110 }
111 Bytes::copy_from_slice(id.to_string().as_bytes())
112}
113
114pub(super) fn carried_id<Id: FromStr>(headers: &HeaderMap) -> Option<Result<Id, String>> {
117 let value = headers.get(OUTBOX_ID_HEADER)?;
118 Some(
119 str::from_utf8(value)
120 .ok()
121 .and_then(|text| text.parse().ok())
122 .ok_or_else(|| String::from_utf8_lossy(value).into_owned()),
123 )
124}
125
126#[cfg(test)]
127mod tests {
128 use ruststream::HeaderMap;
129
130 use super::{ID_ON_STACK, OUTBOX_ID_HEADER, carried_id, id_value};
131
132 #[test]
133 fn an_id_survives_the_header() {
134 let mut headers = HeaderMap::new();
135 headers.insert(OUTBOX_ID_HEADER, 42_i64.to_string());
136 assert_eq!(carried_id::<i64>(&headers), Some(Ok(42)));
137 headers.insert(OUTBOX_ID_HEADER, "7f3a".to_owned());
138 assert_eq!(carried_id::<String>(&headers), Some(Ok("7f3a".to_owned())));
139 }
140
141 #[test]
142 fn an_id_value_holds_the_ids_text_whatever_its_length() {
143 assert_eq!(&id_value(&42_i64)[..], b"42");
144 assert_eq!(&id_value(&i64::MIN)[..], i64::MIN.to_string().as_bytes());
145 let long = "7".repeat(ID_ON_STACK + 1);
146 assert_eq!(&id_value(&long)[..], long.as_bytes());
147 }
148
149 #[test]
150 fn a_missing_or_foreign_header_is_told_apart() {
151 let mut headers = HeaderMap::new();
152 assert_eq!(carried_id::<i64>(&headers), None);
153 headers.insert(OUTBOX_ID_HEADER, "seven");
154 assert_eq!(carried_id::<i64>(&headers), Some(Err("seven".to_owned())));
155 headers.insert(OUTBOX_ID_HEADER, &b"\xff7"[..]);
156 assert_eq!(
157 carried_id::<i64>(&headers),
158 Some(Err("\u{fffd}7".to_owned()))
159 );
160 }
161}