Skip to main content

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}