1use 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
24pub struct Nil;
25
26pub 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#[doc(hidden)]
60pub trait RecordNames: Copy + Send + Sync + 'static {
61 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#[doc(hidden)]
82pub trait RecordList<DB: Database>: RecordNames {
83 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 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 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#[track_caller]
209pub(super) fn defaults<DB: OutboxDatabase, Record: OutboxTable>() -> Defaults {
210 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}