InboxSpec
A table described by hand implements InboxTable: a const InboxSpec chain, its settings as types in type Table, and the same statements as the derive.
The derive and the description by hand are two spellings of one public API, and they build the same statements and the same per-message code.
A service writes the description by hand for a declaration a reviewer reads as it is, or for a struct that holds fewer fields than the table has columns.
TABLE holds the description: InboxSpec::new takes the table's name and the id column, and each setter adds a setting.
Every setter is a const fn, so the description is a constant and costs nothing at run time.
type Id names the id's type, and fn id lends it off the row.
type Table names the chain's final type, because stable Rust does not infer the type of an associated constant; a chain that does not match it is a type mismatch.
The type lists the settings the crate reads, in the order the chain sets them:
- the form:
Lease<Time>orAdvisory; - payload mode:
Payload; - the roles a delivery reads off a row:
Key,Attempt,Headers,HeaderFields; - the time roles with their time type:
RetryAfter<Time>,ProcessedAt<Time>; Fifo, the clockClock<Source>and the openingOpens<Level>;- the events the service writes itself, the markers of
spec::own.
The column-only setters, within, group, priority, data, fetching and selecting_all, leave the type as it is.
A setting the chain leaves out keeps its default: the row lock form, row mode, the host's clock, the database's own opening and the crate's events.
Each role a delivery reads names a small trait the struct implements.
| Rule | Derive | By hand |
|---|---|---|
| an id and a table name | compile | the constructor's arguments |
| one form, one message mode, one clock, one opening; each typed role and each own event once | compile | the setter's bound, at compile time |
| a role in the type has its trait; an own event is implemented | compile | a trait bound, at compile time |
| the form rules of Events | compile | a bound on type Table, at compile time |
| a column-only role set once; a column named once | compile | the constant's evaluation, at build time |
| a lock key names a column the table has | compile | the statement's prepare, at startup |
A refusal at build time names the table and the rule, and points at the crate's check rather than the service's line.
Example
One table, email_jobs, described twice: Welcome with the derive and Reminder by hand, each serving its own group.
- Macros
- Manual
use chrono::{DateTime, Utc};
use ruststream_sqlx::dialect::Column;
use ruststream_sqlx::spec::{Attempt, Lease, Payload};
use ruststream_sqlx::{AttemptRow, InboxSpec, InboxTable, PayloadRow};
/// The derive writes the description from the struct.
#[derive(Inbox, sqlx::FromRow)]
#[inbox(table = "email_jobs")]
struct Welcome {
#[field(id, generated)]
job_id: i64,
#[field(group)]
name: String,
#[field(attempt, generated)]
attempt: i16,
#[field(locked_until)]
locked_until: Option<DateTime<Utc>>,
#[field(payload)]
payload: Vec<u8>,
}
/// The same table by hand: the struct holds only what the service reads.
#[derive(sqlx::FromRow)]
struct Reminder {
job_id: i64,
attempt: i16,
payload: Vec<u8>,
}
impl InboxTable for Reminder {
type Id = i64;
// The settings the crate reads as types, in the order the chain sets them.
type Table = InboxSpec<(Lease<DateTime<Utc>>, Attempt, Payload)>;
const TABLE: Self::Table = InboxSpec::new("email_jobs", Column::new("job_id").generated())
.lease(Column::new("locked_until"))
.group(Column::new("name"))
.attempt(Column::new("attempt").generated())
.payload(Column::new("payload"));
fn id(&self) -> &i64 {
&self.job_id
}
}
impl AttemptRow for Reminder {
type Attempt = i16;
fn attempt(&self) -> &i16 {
&self.attempt
}
}
impl PayloadRow for Reminder {
type Column = Vec<u8>;
fn payload(&self) -> &[u8] {
&self.payload
}
}
#[derive(sqlx::FromRow)]
struct Reminder {
job_id: i64,
attempt: i16,
payload: Vec<u8>,
}
impl InboxTable for Reminder {
type Id = i64;
// The settings the crate reads as types, in the order the chain sets them.
type Table = InboxSpec<(Lease<DateTime<Utc>>, Attempt, Payload)>;
const TABLE: Self::Table = InboxSpec::new("email_jobs", Column::new("job_id").generated())
.lease(Column::new("locked_until"))
.group(Column::new("name"))
.attempt(Column::new("attempt").generated())
.payload(Column::new("payload"));
fn id(&self) -> &i64 {
&self.job_id
}
}
impl AttemptRow for Reminder {
type Attempt = i16;
fn attempt(&self) -> &i16 {
&self.attempt
}
}
impl PayloadRow for Reminder {
type Column = Vec<u8>;
fn payload(&self) -> &[u8] {
&self.payload
}
}
struct Mailer;
impl Handle<Email> for Mailer {
fn handle(
&self,
email: &Email,
_outs: &(),
ctx: &mut Context<'_>,
) -> impl Future<Output = Result<(), HandlerOutcome>> {
tracing::info!(to = %email.to, group = ctx.name(), "sending");
ready(Ok(()))
}
}
/// The service on the database `url` names.
fn service(url: &str) -> RustStream {
let schema_url = url.to_owned();
// Nothing connects here: the broker builds its pool when the service starts.
let broker = SqlxBroker::from_url(url, SqlitePoolOptions::new());
let broker = broker
.route::<Reminder>("welcomes")
.route::<Reminder>("reminders");
RustStream::new(AppInfo::new("mailer", "0.1.0"))
// The table, created before the subscription checks it.
.on_startup(async move |()| {
let mut conn = SqliteConnection::connect(&schema_url).await?;
sqlx::raw_sql(SCHEMA).execute(&mut conn).await?;
conn.close().await
})
.with_broker(broker, |b| {
for group in ["welcomes", "reminders"] {
let send = subscriber(group, Mailer)
.map_source(move |_| InboxQueue::<Reminder>::new(group));
b.include(send.build());
}
})
}