use heck::ToUpperCamelCase;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{format_ident, quote, quote_spanned};
use ruststream_sqlx_dialect::{Opening, Role};
use syn::spanned::Spanned;
use crate::parse::{ColumnField, Custom, Field, Inbox};
use crate::template::KeyItem;
pub(crate) struct Description {
pub(crate) markers: Vec<TokenStream2>,
pub(crate) chain: TokenStream2,
pub(crate) span: Span,
}
impl Description {
pub(crate) fn table_type(&self) -> TokenStream2 {
let markers = &self.markers;
quote_spanned!(self.span=> ::ruststream_sqlx::InboxSpec<(#(#markers,)*)>)
}
pub(crate) fn settings(&self) -> TokenStream2 {
let markers = &self.markers;
quote_spanned!(self.span=> (#(#markers,)*))
}
}
struct Steps {
setters: Vec<TokenStream2>,
markers: Vec<TokenStream2>,
}
impl Steps {
fn push(&mut self, setter: TokenStream2, marker: Option<TokenStream2>) {
self.setters.push(setter);
self.markers.extend(marker);
}
}
pub(crate) fn column(column: &ColumnField) -> TokenStream2 {
let name = &column.name;
let generated = column.generated.then(|| quote!(.generated()));
quote!(::ruststream_sqlx::dialect::Column::new(#name) #generated)
}
fn spanned(field: &Field<'_>) -> TokenStream2 {
let ty = field.ty;
quote_spanned!(ty.span()=> #ty)
}
fn time(field: &Field<'_>) -> TokenStream2 {
let ty = field.ty;
quote_spanned!(ty.span()=> <#ty as ::ruststream_sqlx::TimeColumn>::Time)
}
pub(crate) fn describe(
inbox: &Inbox<'_>,
id: &ColumnField,
key: Option<&[KeyItem]>,
trailing: &[(TokenStream2, TokenStream2)],
) -> Description {
let spec = quote!(::ruststream_sqlx::spec);
let dialect = quote!(::ruststream_sqlx::dialect);
let mut steps = Steps {
setters: Vec::new(),
markers: Vec::new(),
};
if let Some(schema) = &inbox.table.schema {
steps.push(quote!(.within(#schema)), None);
}
let playing = |role: Role| {
inbox
.columns()
.find(|(_, column)| column.role == Some(role))
};
if let Some(key) = key {
let parts = key.iter().map(|item| match item {
KeyItem::Literal(text) => quote!(#dialect::KeyPart::Literal(#text)),
KeyItem::Column(column) => quote!(#dialect::KeyPart::Column(#column)),
});
steps.push(
quote!(.advisory(&[#(#parts),*])),
Some(quote!(#spec::Advisory)),
);
} else if let Some((field, expiry)) = playing(Role::LockedUntil) {
let expiry = column(expiry);
let time = time(field);
steps.push(quote!(.lease(#expiry)), Some(quote!(#spec::Lease<#time>)));
}
for role in Role::ALL {
if let Some((field, slot)) = playing(*role)
&& let Some((setter, marker)) = role_step(*role, field, slot)
{
steps.push(setter, marker);
}
}
let data: Vec<_> = inbox
.columns()
.filter(|(_, column)| column.role.is_none())
.map(|(_, data)| column(data))
.collect();
if !data.is_empty() {
steps.push(quote!(.data(&[#(#data),*])), None);
}
if inbox.flattens() {
steps.push(quote!(.selecting_all()), None);
}
let mut span = Span::call_site();
if let Some(clock) = &inbox.table.clock {
span = clock.span();
steps.push(
quote!(.clock::<#clock>()),
Some(quote!(#spec::Clock<#clock>)),
);
}
if let Some(level) = level(inbox.table.opening) {
steps.push(
quote!(.opens::<#level>()),
Some(quote!(#spec::Opens<#level>)),
);
}
for event in own_events(inbox.table.custom) {
steps.push(quote!(.own::<#event>()), Some(event));
}
for (setter, marker) in trailing {
steps.push(setter.clone(), Some(marker.clone()));
}
let table = &inbox.table.name;
let id = column(id);
let setters = &steps.setters;
Description {
markers: steps.markers,
chain: quote!(::ruststream_sqlx::InboxSpec::new(#table, #id) #(#setters)*),
span,
}
}
fn role_step(
role: Role,
field: &Field<'_>,
slot: &ColumnField,
) -> Option<(TokenStream2, Option<TokenStream2>)> {
let spec = quote!(::ruststream_sqlx::spec);
let value = column(slot);
Some(match role {
Role::Group if slot.fifo.is_some() => {
(quote!(.fifo_group(#value)), Some(quote!(#spec::Fifo)))
}
Role::Group => (quote!(.group(#value)), None),
Role::PartitionKey => (quote!(.partition_key(#value)), Some(quote!(#spec::Key))),
Role::Priority => (quote!(.priority(#value)), None),
Role::RetryAfter => {
let ty = spanned(field);
(
quote!(.retry_after(#value)),
Some(quote!(#spec::RetryAfter<#ty>)),
)
}
Role::Attempt => slot.try_from.as_deref().map_or_else(
|| (quote!(.attempt(#value)), Some(quote!(#spec::Attempt))),
|decoded| {
(
quote!(.attempt_from::<#decoded>(#value)),
Some(quote!(#spec::AttemptFrom<#decoded>)),
)
},
),
Role::ProcessedAt => {
let ty = spanned(field);
(
quote!(.processed_at(#value)),
Some(quote!(#spec::ProcessedAt<#ty>)),
)
}
Role::Headers => (quote!(.headers(#value)), Some(quote!(#spec::Headers))),
Role::Payload => (quote!(.payload(#value)), Some(quote!(#spec::Payload))),
_ => return None,
})
}
fn level(opening: Opening) -> Option<TokenStream2> {
let word = match opening {
Opening::Isolation(level) => level.attribute(),
Opening::Mode(mode) => mode.attribute(),
_ => return None,
};
let name = format_ident!("{}", word.to_upper_camel_case());
Some(quote!(::ruststream_sqlx::dialect::level::#name))
}
pub(crate) fn own_events(custom: Custom) -> Vec<TokenStream2> {
let listed = [
(custom.claim.is_some(), "Claim"),
(custom.fetch, "Fetch"),
(custom.ack, "Ack"),
(custom.retry, "Retry"),
(custom.retry_after, "RetryAfter"),
(custom.discard, "Discard"),
(custom.dead_letter, "DeadLetter"),
(custom.extend.is_some(), "Extend"),
(custom.lock.is_some(), "Lock"),
(custom.unlock.is_some(), "Unlock"),
];
listed
.into_iter()
.filter(|(listed, _)| *listed)
.map(|(_, event)| {
let event = format_ident!("{event}");
quote!(::ruststream_sqlx::spec::own::#event)
})
.collect()
}
#[cfg(test)]
mod tests {
use quote::format_ident;
use syn::{DeriveInput, parse_quote};
use crate::inbox::tests::expanded;
#[test]
fn a_declared_opening_and_clock_reach_the_chain_and_the_type() -> syn::Result<()> {
let cases = [
("isolation", "read_uncommitted", "ReadUncommitted"),
("isolation", "read_committed", "ReadCommitted"),
("isolation", "repeatable_read", "RepeatableRead"),
("isolation", "serializable", "Serializable"),
("mode", "deferred", "Deferred"),
("mode", "immediate", "Immediate"),
("mode", "exclusive", "Exclusive"),
];
for (key, word, name) in cases {
let (key, word) = (format_ident!("{key}"), format_ident!("{word}"));
let input: DeriveInput = parse_quote! {
#[inbox(table = "jobs", #key = #word, clock = DatabaseClock)]
struct Job { #[field(id)] id: i64 }
};
let impls = expanded(&input)?;
let level = format!("::ruststream_sqlx::dialect::level::{name}");
for expected in [
format!(
"typeTable=::ruststream_sqlx::InboxSpec<(::ruststream_sqlx::spec::Clock<DatabaseClock>,\
::ruststream_sqlx::spec::Opens<{level}>,)>;"
),
format!(".clock::<DatabaseClock>().opens::<{level}>();"),
] {
assert!(
impls.contains(&expected),
"{key} = {word}: {expected}\n{impls}"
);
}
}
let input: DeriveInput = parse_quote! {
#[inbox(table = "jobs")]
struct Job { #[field(id)] id: i64 }
};
let impls = expanded(&input)?;
assert!(
impls.contains("typeTable=::ruststream_sqlx::InboxSpec<()>;"),
"{impls}"
);
Ok(())
}
#[test]
fn a_converted_attempt_names_the_type_its_column_decodes_as() -> syn::Result<()> {
let input: DeriveInput = parse_quote! {
#[inbox(table = "jobs")]
struct Job {
#[field(id)] id: i64,
#[field(attempt)] #[sqlx(try_from = "i16")] attempt: u16,
#[field(processed_at)] done: Option<DateTime<Utc>>,
#[field(priority)] rank: i16,
}
};
let impls = expanded(&input)?;
for expected in [
"typeTable=::ruststream_sqlx::InboxSpec<(::ruststream_sqlx::spec::AttemptFrom<i16>,\
::ruststream_sqlx::spec::ProcessedAt<Option<DateTime<Utc>>>,)>;",
".priority(::ruststream_sqlx::dialect::Column::new(\"rank\"))\
.attempt_from::<i16>(::ruststream_sqlx::dialect::Column::new(\"attempt\"))\
.processed_at(::ruststream_sqlx::dialect::Column::new(\"done\"));",
"typeAttempt=u16;",
] {
assert!(impls.contains(expected), "{expected}\n{impls}");
}
Ok(())
}
#[test]
fn an_advisory_lock_key_is_the_forms_setter() -> syn::Result<()> {
let input: DeriveInput = parse_quote! {
#[inbox(table = "ledger", advisory_lock = "ledger-{account}")]
struct Entry { #[field(id)] id: i64, #[sqlx(rename = "acct")] account: String }
};
let impls = expanded(&input)?;
for expected in [
"typeTable=::ruststream_sqlx::InboxSpec<(::ruststream_sqlx::spec::Advisory,)>;",
".advisory(&[::ruststream_sqlx::dialect::KeyPart::Literal(\"ledger-\"),\
::ruststream_sqlx::dialect::KeyPart::Column(\"acct\")])",
] {
assert!(impls.contains(expected), "{expected}\n{impls}");
}
Ok(())
}
#[test]
fn a_listed_lock_and_unlock_are_the_services_own_events() -> syn::Result<()> {
let input: DeriveInput = parse_quote! {
#[inbox(table = "jobs", advisory_lock = "jobs-{id}", custom(lock, unlock))]
struct Job { #[field(id)] id: i64 }
};
let impls = expanded(&input)?;
for expected in [
"::ruststream_sqlx::spec::own::Lock,::ruststream_sqlx::spec::own::Unlock,)>;",
".own::<::ruststream_sqlx::spec::own::Lock>().own::<::ruststream_sqlx::spec::own::Unlock>();",
] {
assert!(impls.contains(expected), "{expected}\n{impls}");
}
Ok(())
}
}