cratestack-macros 0.2.1

Rust-native schema-first framework for typed HTTP APIs, generated clients, and backend services.
Documentation
use cratestack_core::{Model, ModelEventKind, parse_emit_attribute};
use quote::quote;

use crate::shared::{generated_doc_attr, ident, to_snake_case};

pub(crate) fn model_emitted_events(model: &Model) -> Result<Vec<ModelEventKind>, String> {
    let mut emitted = Vec::new();
    for attribute in &model.attributes {
        if attribute.raw.starts_with("@@emit(") {
            emitted.extend(parse_emit_attribute(&attribute.raw)?);
        }
    }
    emitted.sort_by_key(|operation| match operation {
        ModelEventKind::Created => 0,
        ModelEventKind::Updated => 1,
        ModelEventKind::Deleted => 2,
    });
    emitted.dedup();
    Ok(emitted)
}

pub(crate) fn generate_event_module(models: &[Model]) -> Result<proc_macro2::TokenStream, String> {
    let aliases = models
        .iter()
        .map(generate_model_event_aliases)
        .collect::<Vec<_>>();
    let methods = models
        .iter()
        .map(generate_model_event_methods)
        .collect::<Result<Vec<_>, String>>()?;

    Ok(quote! {
        pub mod events {
            #(#aliases)*

            #[derive(Clone, Copy)]
            pub struct Subscriptions<'a> {
                runtime: &'a ::cratestack::__private::SqlxRuntime,
            }

            impl<'a> Subscriptions<'a> {
                pub(crate) fn new(runtime: &'a ::cratestack::__private::SqlxRuntime) -> Self {
                    Self { runtime }
                }

                #[doc = "Drains pending model events from the transactional outbox."]
                pub async fn drain(&self) -> Result<usize, ::cratestack::CoolError> {
                    self.runtime.drain_event_outbox().await
                }

                #(#methods)*
            }
        }
    })
}

fn generate_model_event_aliases(model: &Model) -> proc_macro2::TokenStream {
    let model_ident = ident(&model.name);
    let created_ident = ident(&format!("{}CreatedEvent", model.name));
    let updated_ident = ident(&format!("{}UpdatedEvent", model.name));
    let deleted_ident = ident(&format!("{}DeletedEvent", model.name));

    quote! {
        pub type #created_ident = ::cratestack::ModelEvent<super::models::#model_ident>;
        pub type #updated_ident = ::cratestack::ModelEvent<super::models::#model_ident>;
        pub type #deleted_ident = ::cratestack::ModelEvent<super::models::#model_ident>;
    }
}

fn generate_model_event_methods(model: &Model) -> Result<proc_macro2::TokenStream, String> {
    let model_name = &model.name;
    let model_ident = ident(model_name);
    let model_snake = to_snake_case(model_name);
    let methods = model_emitted_events(model)
        .map_err(|error| format!("failed to parse emitted events for `{model_name}`: {error}"))?
        .into_iter()
        .map(|operation| {
            let (method_ident, alias_ident, kind_tokens, docs) = match operation {
                ModelEventKind::Created => (
                    ident(&format!("on_{}_created", model_snake)),
                    ident(&format!("{}CreatedEvent", model_name)),
                    quote! { ::cratestack::ModelEventKind::Created },
                    generated_doc_attr(format!(
                        "Registers a subscriber for `{}` create events.",
                        model_name
                    )),
                ),
                ModelEventKind::Updated => (
                    ident(&format!("on_{}_updated", model_snake)),
                    ident(&format!("{}UpdatedEvent", model_name)),
                    quote! { ::cratestack::ModelEventKind::Updated },
                    generated_doc_attr(format!(
                        "Registers a subscriber for `{}` update events.",
                        model_name
                    )),
                ),
                ModelEventKind::Deleted => (
                    ident(&format!("on_{}_deleted", model_snake)),
                    ident(&format!("{}DeletedEvent", model_name)),
                    quote! { ::cratestack::ModelEventKind::Deleted },
                    generated_doc_attr(format!(
                        "Registers a subscriber for `{}` delete events.",
                        model_name
                    )),
                ),
            };

            quote! {
                #docs
                pub fn #method_ident<F, Fut>(&self, handler: F)
                where
                    F: Fn(#alias_ident) -> Fut + Send + Sync + 'static,
                    Fut: ::core::future::Future<Output = Result<(), ::cratestack::CoolError>>
                        + Send
                        + 'static,
                {
                    let handler = ::std::sync::Arc::new(handler);
                    self.runtime.subscribe(#model_name, #kind_tokens, move |event| {
                        let handler = ::std::sync::Arc::clone(&handler);
                        ::std::boxed::Box::pin(async move {
                            let typed = <::cratestack::ModelEvent<super::models::#model_ident> as ::core::convert::TryFrom<::cratestack::CoolEventEnvelope>>::try_from(event)?;
                            (handler)(typed).await
                        })
                    });
                }
            }
        })
        .collect::<Vec<_>>();

    Ok(quote! {
        #(#methods)*
    })
}