ruststream_sqlx/inbox/publish/repository.rs
1//! `Repository`: the typed publish policy of a row with `Publish`, and its publisher.
2
3use std::any::type_name;
4use std::fmt;
5use std::future::{Future, ready};
6use std::marker::PhantomData;
7use std::sync::Arc;
8
9use ruststream::{Lend, OutgoingMessage, PairError, PublishPolicy, Publisher};
10use ruststream_sqlx_dialect::Dialect;
11use sqlx::Database;
12
13use super::{TableWake, write};
14use crate::inbox::broker::{ConnectedSqlxBroker, Shared};
15use crate::inbox::database::QueueDatabase;
16use crate::inbox::engine::Events;
17use crate::inbox::error::SqlxBrokerError;
18use crate::inbox::events::Publish;
19
20/// The typed publish policy of a row with [`Publish`]: a message published through it becomes a
21/// row of `Row`'s table.
22///
23/// It names its table at compile time, so a publish costs a static call; a row without `Publish`
24/// does not compile as a policy.
25///
26/// # Examples
27///
28/// ```
29/// # #[cfg(feature = "postgres")]
30/// # mod demo {
31/// use ruststream::OutgoingMessage;
32/// use ruststream::prelude::*;
33/// use ruststream_sqlx::{Inbox, InboxQueue, Publish, Repository, SqlxBroker};
34/// use serde::{Deserialize, Serialize};
35/// use sqlx::{PgConnection, PgPool, Postgres};
36///
37/// #[derive(Inbox, sqlx::FromRow)]
38/// #[inbox(table = "jobs")]
39/// pub struct Job {
40/// #[field(id, generated)]
41/// id: i64,
42/// #[field(group)]
43/// name: String,
44/// #[field(payload)]
45/// payload: Vec<u8>,
46/// }
47///
48/// impl Publish<Postgres> for Job {
49/// async fn publish(
50/// conn: &mut PgConnection,
51/// message: &OutgoingMessage<'_>,
52/// ) -> Result<(), sqlx::Error> {
53/// sqlx::query("INSERT INTO jobs (name, payload) VALUES ($1, $2)")
54/// .bind(message.name())
55/// .bind(message.payload())
56/// .execute(conn)
57/// .await?;
58/// Ok(())
59/// }
60/// }
61///
62/// #[derive(Deserialize)]
63/// struct Signup {
64/// email: String,
65/// }
66///
67/// #[derive(Serialize, Outgoing)]
68/// #[outgoing(name = "welcome")]
69/// struct Welcome {
70/// email: String,
71/// }
72///
73/// #[subscriber(InboxQueue::<Job>::new("signups"), reply)]
74/// async fn greet(signup: &Signup) -> Welcome {
75/// Welcome { email: signup.email.clone() }
76/// }
77///
78/// pub fn app(pool: PgPool) -> RustStream {
79/// RustStream::new(AppInfo::new("signup", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
80/// // The welcome becomes a row of `jobs` in group `welcome`, with no route to look up.
81/// b.include(greet).out_reply(Repository::<Job>::default());
82/// })
83/// }
84/// # }
85/// # fn main() {}
86/// ```
87pub struct Repository<Row>(PhantomData<fn() -> Row>);
88
89impl<Row> Default for Repository<Row> {
90 fn default() -> Self {
91 Self(PhantomData)
92 }
93}
94
95impl<Row> Clone for Repository<Row> {
96 fn clone(&self) -> Self {
97 *self
98 }
99}
100
101impl<Row> Copy for Repository<Row> {}
102
103impl<Row> fmt::Debug for Repository<Row> {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 write!(f, "Repository<{}>", type_name::<Row>())
106 }
107}
108
109impl<DB, D, Row> PublishPolicy<ConnectedSqlxBroker<DB, D>> for Repository<Row>
110where
111 DB: QueueDatabase,
112 D: Dialect + 'static,
113 Row: Publish<DB> + Events<DB>,
114{
115 type Live = RepositoryPublisher<DB, Row>;
116
117 fn pair(
118 self,
119 connected: &ConnectedSqlxBroker<DB, D>,
120 ) -> impl Future<Output = Result<Self::Live, PairError>> + Send {
121 ready(Ok(RepositoryPublisher {
122 shared: Arc::clone(&connected.shared),
123 wake: connected.shared.wakes.table(&Row::SPEC),
124 _row: PhantomData,
125 }))
126 }
127}
128
129/// The live form of a [`Repository`]: publishes into `Row`'s table through its [`Publish`].
130///
131/// After the broker shut down it answers [`SqlxBrokerError::Closed`], never a success.
132///
133/// # Examples
134///
135/// ```no_run
136/// # #[cfg(feature = "postgres")]
137/// # mod demo {
138/// use ruststream::OutgoingMessage;
139/// use ruststream_sqlx::prelude::*;
140/// use serde::Serialize;
141/// use sqlx::{PgConnection, PgPool, Postgres};
142///
143/// #[derive(Inbox, sqlx::FromRow)]
144/// #[inbox(table = "report_jobs")]
145/// pub struct ReportJob {
146/// #[field(id, generated)]
147/// id: i64,
148/// #[field(payload)]
149/// payload: Vec<u8>,
150/// }
151///
152/// impl Publish<Postgres> for ReportJob {
153/// async fn publish(
154/// conn: &mut PgConnection,
155/// message: &OutgoingMessage<'_>,
156/// ) -> Result<(), sqlx::Error> {
157/// sqlx::query("INSERT INTO report_jobs (payload) VALUES ($1)")
158/// .bind(message.payload())
159/// .execute(conn)
160/// .await?;
161/// Ok(())
162/// }
163/// }
164///
165/// #[derive(Serialize, Outgoing)]
166/// #[outgoing(name = "reports")]
167/// pub struct Report {
168/// month: u32,
169/// }
170///
171/// pub async fn run(pool: PgPool) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
172/// let broker = SqlxBroker::new(pool).bindable();
173/// let reports = broker.bind(Repository::<ReportJob>::default());
174/// let running = RustStream::new(AppInfo::new("reports", "0.1.0"))
175/// .with_broker(broker, |_b| {})
176/// .start()
177/// .await?;
178///
179/// // A task scheduled from outside any handler: the service's HTTP endpoint owns this
180/// // `RepositoryPublisher` and writes each request into `report_jobs`.
181/// let publisher = running.publisher(reports).await?;
182/// publisher.message(&Report { month: 10 }).publish().await?;
183///
184/// running.shutdown().await?;
185/// Ok(())
186/// }
187/// # }
188/// # fn main() {}
189/// ```
190pub struct RepositoryPublisher<DB: Database, Row> {
191 shared: Arc<Shared<DB>>,
192 /// The subscriptions of `Row`'s table on the connection, woken after each write.
193 wake: &'static TableWake,
194 _row: PhantomData<fn() -> Row>,
195}
196
197impl<DB: Database, Row> fmt::Debug for RepositoryPublisher<DB, Row> {
198 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199 write!(f, "RepositoryPublisher<{}>", type_name::<Row>())
200 }
201}
202
203impl<DB, Row> Publisher for RepositoryPublisher<DB, Row>
204where
205 DB: QueueDatabase,
206 Row: Publish<DB> + Events<DB>,
207{
208 type Payload = Lend;
209 type Error = SqlxBrokerError;
210 type Options = ();
211
212 async fn publish(
213 &self,
214 msg: OutgoingMessage<'_>,
215 _options: Option<&()>,
216 ) -> Result<(), SqlxBrokerError> {
217 write::<DB, Row>(&self.shared, self.wake, &msg).await
218 }
219}