use std::env;
use ruststream::schemars::JsonSchema;
use serde::Deserialize;
use sqlx::postgres::{PgPoolOptions, Postgres};
use ruststream_sqlx::prelude::*;
#[derive(Inbox, sqlx::FromRow)]
#[inbox(table = "email_jobs", schema = "app")]
struct SendEmail {
#[field(id, generated)]
id: i64,
#[field(group)]
name: String,
#[field(payload)]
payload: Vec<u8>,
}
#[derive(Inbox, sqlx::FromRow)]
#[inbox(table = "reports")]
struct RenderReport {
#[field(id, generated)]
id: i64,
#[field(payload)]
payload: Vec<u8>,
}
#[subscriber(InboxQueue::<SendEmail>::new("emails"))]
async fn send_email(email: &Email) -> HandlerOutcome {
tracing::info!(to = %email.to, "sending");
HandlerOutcome::ack()
}
#[subscriber(InboxQueue::<RenderReport>::new("reports"))]
async fn render(report: &Report) -> HandlerOutcome {
tracing::info!(month = report.month, "rendering");
HandlerOutcome::ack()
}
#[derive(Debug, Clone, Inbox, sqlx::FromRow, JsonSchema)]
#[schemars(crate = "ruststream::schemars")]
#[inbox(table = "mail_jobs")]
struct Mail {
#[field(id, generated)]
id: i64,
#[field(group)]
name: String,
recipient: String,
subject: Option<String>,
}
#[subscriber(InboxQueue::<Mail>::new("mail"))]
async fn send_mail(mail: &Mail) -> HandlerOutcome {
tracing::info!(to = %mail.recipient, subject = ?mail.subject, "mailing");
HandlerOutcome::ack()
}
fn broker(url: &str) -> SqlxBroker<Postgres> {
SqlxBroker::from_url(url, PgPoolOptions::new().max_connections(8))
}
fn service(url: &str) -> RustStream {
RustStream::new(AppInfo::new("office", "0.1.0")).with_broker_labeled(
"postgres",
broker(url),
|b| {
b.include(send_email);
b.include(render);
b.include(send_mail);
},
)
}
#[ruststream::app]
fn app() -> impl App {
let url = env::var("DATABASE_URL").unwrap_or_else(|_| "postgres://localhost/office".to_owned());
service(&url)
}
#[derive(Deserialize, JsonSchema)]
#[schemars(crate = "ruststream::schemars")]
struct Email {
to: String,
}
#[derive(Deserialize, JsonSchema)]
#[schemars(crate = "ruststream::schemars")]
struct Report {
month: u32,
}
#[cfg(test)]
mod tests {
use std::error::Error;
use ruststream::asyncapi::build_spec;
use serde_json::{Value, json};
use super::service;
const PASSWORD: &str = "hunter2";
fn document() -> Result<Value, Box<dyn Error>> {
let url = format!("postgres://office:{PASSWORD}@db.internal:5432/office");
Ok(serde_json::from_str(
&build_spec(&service(&url)).to_json()?,
)?)
}
#[tokio::test]
async fn the_server_is_the_pools_host_and_port() -> Result<(), Box<dyn Error>> {
let spec = document()?;
assert_eq!(
spec["servers"]["postgres"],
json!({ "host": "db.internal:5432", "protocol": "postgres" })
);
Ok(())
}
#[tokio::test]
async fn the_document_carries_no_password() -> Result<(), Box<dyn Error>> {
let text = document()?.to_string();
assert!(!text.contains(PASSWORD), "{text}");
Ok(())
}
#[tokio::test]
async fn each_channel_names_its_table_and_group() -> Result<(), Box<dyn Error>> {
let spec = document()?;
let channels = &spec["channels"];
assert_eq!(
channels["emails"]["bindings"]["x-ruststream-sqlx"],
json!({ "table": "app.email_jobs", "group": "emails" })
);
assert_eq!(
channels["reports"]["bindings"]["x-ruststream-sqlx"],
json!({ "table": "reports" })
);
Ok(())
}
#[tokio::test]
async fn a_row_handler_documents_the_rows_columns() -> Result<(), Box<dyn Error>> {
let text = document()?.to_string();
for column in ["recipient", "subject"] {
assert!(
text.contains(column),
"the row's {column} is documented: {text}"
);
}
Ok(())
}
}