use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use super::context::Context;
pub const OUTBOX_INSERT_SQL: &str = "INSERT INTO entity_outbox (entity, kind, entity_id, payload) \
VALUES ($1, $2, $3, $4)";
impl Context<'_> {
fn outbox_enqueue(&self, kind: &str, entity_id_expr: &TokenStream) -> TokenStream {
if !self.outbox {
return TokenStream::new();
}
let table = &self.table;
quote! {
let __outbox_payload = ::serde_json::to_value(&__event)
.expect("event serialization should not fail");
::sqlx::query(#OUTBOX_INSERT_SQL)
.bind(#table)
.bind(#kind)
.bind(#entity_id_expr)
.bind(&__outbox_payload)
.execute(&mut *tx)
.await?;
}
}
pub fn outbox_created(&self) -> TokenStream {
if !self.outbox {
return TokenStream::new();
}
let entity_name = self.entity_name;
let event_name = format_ident!("{}Event", entity_name);
let id_name = self.id_name;
let enqueue = self.outbox_enqueue("created", "e! { entity.#id_name.to_string() });
quote! {
{
let __event = #event_name::created(entity.clone());
#enqueue
}
}
}
pub fn outbox_updated(&self) -> TokenStream {
if !self.outbox {
return TokenStream::new();
}
let entity_name = self.entity_name;
let event_name = format_ident!("{}Event", entity_name);
let id_name = self.id_name;
let enqueue = self.outbox_enqueue("updated", "e! { entity.#id_name.to_string() });
quote! {
{
let __event = #event_name::updated(__old_entity.clone(), entity.clone());
#enqueue
}
}
}
pub fn outbox_deleted(&self) -> TokenStream {
if !self.outbox {
return TokenStream::new();
}
let entity_name = self.entity_name;
let event_name = format_ident!("{}Event", entity_name);
let (constructor, kind) = if self.soft_delete {
(format_ident!("soft_deleted"), "soft_deleted")
} else {
(format_ident!("hard_deleted"), "hard_deleted")
};
let enqueue = self.outbox_enqueue(kind, "e! { id.to_string() });
quote! {
{
let __event = #event_name::#constructor(id);
#enqueue
}
}
}
}
#[cfg(test)]
mod tests {
use quote::quote;
use syn::DeriveInput;
use super::super::context::Context;
use crate::entity::parse::EntityDef;
fn parse_entity(tokens: proc_macro2::TokenStream) -> EntityDef {
let input: DeriveInput = syn::parse2(tokens).expect("test entity must parse");
EntityDef::from_derive_input(&input).expect("test entity must be valid")
}
fn outbox_entity() -> EntityDef {
parse_entity(quote! {
#[entity(table = "users", events(outbox))]
pub struct User {
#[id]
pub id: uuid::Uuid,
#[field(create, update, response)]
pub name: String,
}
})
}
#[test]
fn created_fragment_inserts_into_outbox() {
let entity = outbox_entity();
let code = Context::new(&entity).outbox_created().to_string();
assert!(code.contains("entity_outbox"));
assert!(code.contains("\"created\""));
assert!(code.contains("\"users\""));
}
#[test]
fn deleted_fragment_uses_hard_deleted_without_soft_delete() {
let entity = outbox_entity();
let code = Context::new(&entity).outbox_deleted().to_string();
assert!(code.contains("hard_deleted"));
}
#[test]
fn deleted_fragment_uses_soft_deleted_with_soft_delete() {
let entity = parse_entity(quote! {
#[entity(table = "users", events(outbox), soft_delete)]
pub struct User {
#[id]
pub id: uuid::Uuid,
#[field(create, update, response)]
pub name: String,
pub deleted_at: Option<chrono::DateTime<chrono::Utc>>,
}
});
let code = Context::new(&entity).outbox_deleted().to_string();
assert!(code.contains("soft_deleted"));
}
#[test]
fn fragments_empty_without_outbox() {
let entity = parse_entity(quote! {
#[entity(table = "users", events)]
pub struct User {
#[id]
pub id: uuid::Uuid,
#[field(create, update, response)]
pub name: String,
}
});
let ctx = Context::new(&entity);
assert!(ctx.outbox_created().is_empty());
assert!(ctx.outbox_updated().is_empty());
assert!(ctx.outbox_deleted().is_empty());
}
}