Skip to main content

ruststream_sqlx/outbox/registry/
list.rs

1//! The type-level list of registrations: each node holds its name and its record's default
2//! statements, and a lookup walks the list comparing names.
3
4use std::any::type_name;
5use std::fmt;
6use std::future::{Future, ready};
7use std::marker::PhantomData;
8
9use ruststream::runtime::{Context, Handler, HandlerOutcome};
10use ruststream::{Bytes, OutgoingMessage, Publisher};
11use sqlx::Database;
12
13use crate::outbox::database::{Defaults, OutboxDatabase};
14use crate::outbox::error::OutboxError;
15use crate::outbox::events::Tracked;
16use crate::outbox::layer::deliver;
17use crate::outbox::publish::record;
18use crate::outbox::republish::recover_and_publish;
19use crate::outbox::spec::{Described, OutboxTable};
20use crate::outbox::store::Store;
21
22/// The end of the registrations.
23#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
24pub struct Nil;
25
26/// A name the outbox tracks with `Record`, in front of the registrations `Rest`.
27pub struct Registered<Record, Rest> {
28    pub(super) name: &'static str,
29    pub(super) defaults: Defaults,
30    pub(super) rest: Rest,
31    pub(super) record: PhantomData<fn() -> Record>,
32}
33
34impl<Record, Rest: Clone> Clone for Registered<Record, Rest> {
35    fn clone(&self) -> Self {
36        Self {
37            name: self.name,
38            defaults: self.defaults,
39            rest: self.rest.clone(),
40            record: PhantomData,
41        }
42    }
43}
44
45impl<Record, Rest: Copy> Copy for Registered<Record, Rest> {}
46
47impl<Record, Rest: fmt::Debug> fmt::Debug for Registered<Record, Rest> {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        f.debug_struct("Registered")
50            .field("name", &self.name)
51            .field("defaults", &self.defaults)
52            .field("record", &type_name::<Record>())
53            .field("rest", &self.rest)
54            .finish()
55    }
56}
57
58/// The names of a registry, whatever their record types.
59#[doc(hidden)]
60pub trait RecordNames: Copy + Send + Sync + 'static {
61    /// Whether `name` is registered.
62    fn contains(&self, name: &str) -> bool;
63}
64
65impl RecordNames for Nil {
66    #[inline]
67    fn contains(&self, _name: &str) -> bool {
68        false
69    }
70}
71
72impl<Record: 'static, Rest: RecordNames> RecordNames for Registered<Record, Rest> {
73    #[inline]
74    fn contains(&self, name: &str) -> bool {
75        self.name == name || self.rest.contains(name)
76    }
77}
78
79/// What the registry does for the record type of a name; each method finds the name's node and
80/// runs the record's events there.
81#[doc(hidden)]
82pub trait RecordList<DB: Database>: RecordNames {
83    /// Records `msg` under its name; `None` when the name is not registered, else the id header's
84    /// value.
85    fn record<'a>(
86        &'a self,
87        pool: &'a Store<DB>,
88        msg: &'a OutgoingMessage<'_>,
89    ) -> impl Future<Output = Option<Result<Bytes, OutboxError>>> + Send + 'a;
90
91    /// Runs `handler` on a delivery under a registered name, taking and settling its record.
92    fn deliver<'a, M, C, S, H>(
93        &'a self,
94        pool: &'a Store<DB>,
95        handler: &'a H,
96        msg: &'a M,
97        ctx: &'a mut Context<'_, C, S>,
98    ) -> impl Future<Output = HandlerOutcome> + Send + 'a
99    where
100        M: Sync,
101        C: Send,
102        S: Send + Sync,
103        H: Handler<M, C, S>;
104
105    /// Publishes the unprocessed records of every name `only` lists (every name when `None`)
106    /// through `publisher`.
107    fn republish<'a, Live: Publisher>(
108        &'a self,
109        pool: &'a Store<DB>,
110        publisher: &'a Live,
111        only: Option<&'a [&'static str]>,
112    ) -> impl Future<Output = Result<(), OutboxError>> + Send + 'a;
113}
114
115impl<DB: Database> RecordList<DB> for Nil {
116    fn record<'a>(
117        &'a self,
118        _pool: &'a Store<DB>,
119        _msg: &'a OutgoingMessage<'_>,
120    ) -> impl Future<Output = Option<Result<Bytes, OutboxError>>> + Send + 'a {
121        ready(None)
122    }
123
124    fn deliver<'a, M, C, S, H>(
125        &'a self,
126        _pool: &'a Store<DB>,
127        handler: &'a H,
128        msg: &'a M,
129        ctx: &'a mut Context<'_, C, S>,
130    ) -> impl Future<Output = HandlerOutcome> + Send + 'a
131    where
132        M: Sync,
133        C: Send,
134        S: Send + Sync,
135        H: Handler<M, C, S>,
136    {
137        handler.handle(msg, ctx)
138    }
139
140    fn republish<'a, Live: Publisher>(
141        &'a self,
142        _pool: &'a Store<DB>,
143        _publisher: &'a Live,
144        _only: Option<&'a [&'static str]>,
145    ) -> impl Future<Output = Result<(), OutboxError>> + Send + 'a {
146        ready(Ok(()))
147    }
148}
149
150impl<DB, Record, Rest> RecordList<DB> for Registered<Record, Rest>
151where
152    DB: Database,
153    Record: Tracked<DB>,
154    Rest: RecordList<DB>,
155{
156    async fn record<'a>(
157        &'a self,
158        pool: &'a Store<DB>,
159        msg: &'a OutgoingMessage<'_>,
160    ) -> Option<Result<Bytes, OutboxError>> {
161        if msg.name() == self.name {
162            Some(record::<DB, Record>(self.name, pool, msg).await)
163        } else {
164            self.rest.record(pool, msg).await
165        }
166    }
167
168    async fn deliver<'a, M, C, S, H>(
169        &'a self,
170        pool: &'a Store<DB>,
171        handler: &'a H,
172        msg: &'a M,
173        ctx: &'a mut Context<'_, C, S>,
174    ) -> HandlerOutcome
175    where
176        M: Sync,
177        C: Send,
178        S: Send + Sync,
179        H: Handler<M, C, S>,
180    {
181        if ctx.name() == self.name {
182            deliver::<DB, Record, M, C, S, H>(self.name, &self.defaults, pool, handler, msg, ctx)
183                .await
184        } else {
185            self.rest.deliver(pool, handler, msg, ctx).await
186        }
187    }
188
189    async fn republish<'a, Live: Publisher>(
190        &'a self,
191        pool: &'a Store<DB>,
192        publisher: &'a Live,
193        only: Option<&'a [&'static str]>,
194    ) -> Result<(), OutboxError> {
195        if only.is_none_or(|names| names.contains(&self.name)) {
196            recover_and_publish::<DB, Record, Live>(self.name, &self.defaults, pool, publisher)
197                .await?;
198        }
199        self.rest.republish(pool, publisher, only).await
200    }
201}
202
203/// The default statements of `Record` on `DB`, built from its table's description.
204///
205/// # Panics
206///
207/// Panics when the table does not fit a dialect of `DB`.
208#[track_caller]
209pub(super) fn defaults<DB: OutboxDatabase, Record: OutboxTable>() -> Defaults {
210    // Why at run time: the dialects write their statements at run time; the derive checks the
211    // same description while the service compiles.
212    DB::defaults(&Record::TABLE.spec()).unwrap_or_else(|error| {
213        panic!(
214            "`{}` describes an outbox table its database cannot run: {error}",
215            type_name::<Record>()
216        )
217    })
218}
219
220#[cfg(test)]
221pub(super) mod tests {
222    use super::{Defaults, Nil, PhantomData, RecordNames, Registered};
223
224    pub(in crate::outbox::registry) fn node<Rest>(
225        name: &'static str,
226        rest: Rest,
227    ) -> Registered<(), Rest> {
228        Registered {
229            name,
230            defaults: Defaults::default(),
231            rest,
232            record: PhantomData,
233        }
234    }
235
236    #[test]
237    fn a_lookup_finds_every_registered_name_and_nothing_else() {
238        let names = node("orders", node("refunds", node("audits", Nil)));
239        assert!(names.contains("orders"));
240        assert!(names.contains("refunds"));
241        assert!(names.contains("audits"));
242        assert!(!names.contains("notes"));
243        assert!(!names.contains("order"));
244        assert!(!names.contains(""));
245        assert!(!Nil.contains("orders"));
246    }
247}