Skip to main content

ruststream_macros/
lib.rs

1//! Procedural macros for [RustStream](https://github.com/powersemmi/ruststream).
2//!
3//! Re-exported from the `ruststream` crate under the `macros` feature; depend on that rather than
4//! on this crate directly.
5
6mod expand;
7mod from_ref;
8mod parse;
9
10use proc_macro::TokenStream;
11use proc_macro2::TokenStream as TokenStream2;
12use quote::quote;
13use syn::{DeriveInput, ItemFn, parse_macro_input};
14
15use parse::{SubscriberArgs, doc_description};
16
17/// Turns an `async fn` handler into a mountable subscriber definition.
18///
19/// ```ignore
20/// /// Processes incoming orders.
21/// #[subscriber("orders")]
22/// async fn handle(order: &Order) -> HandlerResult { HandlerResult::Ack }
23/// // later: broker_scope.include(handle);
24///
25/// // reply form: the return value is encoded and published to "responses" through the
26/// // TypedPublisher (broker + reply codec) passed at wiring time.
27/// #[subscriber("requests", publish("responses"))]
28/// async fn reply(req: &Request) -> Response { /* ... */ }
29/// // later: broker_scope.include_publishing(reply, typed_publisher);
30///
31/// // reply form with explicit ack control: `Ok` publishes the reply, `Err` skips it and the
32/// // dispatcher acts on the returned HandlerResult.
33/// #[subscriber("requests", publish("responses"))]
34/// async fn confirm(req: &Request) -> Result<Response, HandlerResult> { /* ... */ }
35///
36/// // batch form: the handler takes the whole decoded batch as a slice; the source's
37/// // subscriber must implement BatchSubscriber. Mounted with include_batch.
38/// #[subscriber(batch("orders"))]
39/// async fn bill(orders: &[Order]) -> HandlerResult { /* settles the whole batch */ }
40/// ```
41///
42/// Without `publish(..)` the handler returns any `Into<Settle>` (a `Settle`, a `HandlerResult`,
43/// `()`, or `Result<_, E>`). Attach a post-settle continuation with `HandlerResult::ack().and_after`
44/// (any outcome works), which runs after the message is settled. With `publish(..)` it returns the
45/// reply value to publish, or `Result<Reply, HandlerResult>` to control acknowledgement:
46/// `Err(result)` publishes nothing and returns `result` to the dispatcher. The `Result` form is
47/// detected syntactically, so spell it out in the signature (a type alias is treated as a plain
48/// reply type).
49///
50/// Wrapping the source in `batch(..)` switches the definition to a `BatchDef`: the handler takes
51/// `&[T]` and runs once per batch pulled from the broker's `BatchSubscriber` (use the `Buffered`
52/// adapter for brokers without native batching). It returns any `IntoBatchResult` - one outcome
53/// for the whole batch (`HandlerResult`, `()`, `Result<_, E>`), or a per-element vector
54/// (`Vec<Settle>`, or `Vec<HandlerResult>`) to settle element `i` of the slice with outcome `i`,
55/// each element carrying its own optional `and_after` continuation. The source type is recovered
56/// from the constructor path, so a generic source spells its parameters:
57/// `batch(Buffered::<Name>::new(Name::new("orders")))`.
58///
59/// Combining `batch(..)` with `publish(..)` produces a `BatchPublishingDef` (mounted with
60/// `include_batch_publishing`): the handler returns `Vec<Reply>` (or
61/// `Result<Vec<Reply>, HandlerResult>` for explicit ack control, all-or-nothing - selective
62/// outcomes do not compose with a transaction), every reply is published to the reply name, and
63/// the whole batch is acked after. Hand the mount a `TypedPublisher` for independent reply
64/// publishes, or `.transactional()` for one transaction per batch.
65///
66/// A `workers(n)` clause processes up to `n` deliveries (or batches) of this subscriber
67/// concurrently, each in its own task; global processing order is lost by design, and
68/// back-pressure holds at `n` in-flight deliveries. `workers(n, by_key)` switches to `n`
69/// sequential lanes keyed by the message's partition key, preserving per-key ordering
70/// (single-message forms only). The default is the sequential loop.
71///
72/// In both forms the handler may declare an optional second parameter, the per-delivery
73/// `&mut Context`, to read app state or publish manually. Any further parameter is an extractor: its
74/// type must implement
75/// [`FromContext`](../ruststream/runtime/trait.FromContext.html), and the generated handler resolves
76/// it from the delivery context (in declaration order) before the body runs, so dependencies arrive
77/// as arguments. A failed extraction settles the delivery by the rejection's `HandlerResult` without
78/// running the body.
79///
80/// ```ignore
81/// // `State<Db>` is resolved from the application state before the body runs.
82/// #[subscriber("orders")]
83/// async fn handle(order: &Order, State(db): State<Db>) -> HandlerResult { /* ... */ }
84/// ```
85#[proc_macro_attribute]
86pub fn subscriber(attr: TokenStream, item: TokenStream) -> TokenStream {
87    let args = parse_macro_input!(attr as SubscriberArgs);
88    let func = parse_macro_input!(item as ItemFn);
89    expand::subscriber(&args, &func).unwrap_or_else(|err| err.to_compile_error().into())
90}
91
92/// Generates a `main` entry point for a `RustStream` service.
93///
94/// Place it on a synchronous, argument-free function that builds and returns an application -
95/// `impl App` (the recommended form, hiding the composed type parameters) or a concrete
96/// `RustStream<_>`. The expansion keeps the function and adds a `main` that hands it to
97/// `ruststream::runtime::cli::run_main`, producing a binary that understands the `run` and
98/// `asyncapi gen` commands with no hand-written runtime boilerplate.
99///
100/// ```ignore
101/// #[ruststream::app]
102/// fn app() -> impl App {
103///     RustStream::new(AppInfo::new("svc", "0.1.0")).register_broker(MemoryBroker::new())
104/// }
105/// ```
106#[proc_macro_attribute]
107pub fn app(attr: TokenStream, item: TokenStream) -> TokenStream {
108    let func = parse_macro_input!(item as ItemFn);
109    expand_app(&attr.into(), &func).unwrap_or_else(|err| err.to_compile_error().into())
110}
111
112fn expand_app(attr: &TokenStream2, func: &ItemFn) -> syn::Result<TokenStream> {
113    if !attr.is_empty() {
114        return Err(syn::Error::new_spanned(
115            attr,
116            "#[ruststream::app] takes no arguments",
117        ));
118    }
119    if let Some(asyncness) = func.sig.asyncness {
120        return Err(syn::Error::new_spanned(
121            asyncness,
122            "#[ruststream::app] requires a synchronous builder returning `impl App` or `RustStream`",
123        ));
124    }
125    if !func.sig.inputs.is_empty() {
126        return Err(syn::Error::new_spanned(
127            &func.sig.inputs,
128            "#[ruststream::app] builder must take no arguments",
129        ));
130    }
131    let name = &func.sig.ident;
132    Ok(quote! {
133        #func
134
135        fn main() -> ::std::process::ExitCode {
136            ::ruststream::runtime::cli::run_main(#name)
137        }
138    }
139    .into())
140}
141
142/// Derives [`Message`](../ruststream/trait.Message.html) metadata: the type name and its doc
143/// comment.
144///
145/// ```ignore
146/// /// An order placed by a customer.
147/// #[derive(Message)]
148/// struct Order { id: u32 }
149/// // Order::NAME == "Order", Order::DESCRIPTION == Some("An order placed by a customer.")
150/// ```
151#[proc_macro_derive(Message)]
152pub fn derive_message(item: TokenStream) -> TokenStream {
153    let input = parse_macro_input!(item as DeriveInput);
154    let name = &input.ident;
155    let name_str = name.to_string();
156    let description = doc_description(&input.attrs);
157    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
158
159    quote! {
160        impl #impl_generics ::ruststream::Message for #name #ty_generics #where_clause {
161            const NAME: &'static str = #name_str;
162            const DESCRIPTION: ::core::option::Option<&'static str> = #description;
163        }
164    }
165    .into()
166}
167
168/// Derives [`FromRef`](../ruststream/runtime/trait.FromRef.html) for each field of an
169/// application-state struct, so `#[subscriber]` handlers can inject any field with
170/// `State<FieldType>` without a hand-written impl.
171///
172/// Each field gets a `FromRef` impl that clones it out of the state. Because the generated impl
173/// carries no generic parameter, it is legal even for fields whose type comes from another crate (a
174/// broker publisher, a client pool). A field that another field's type already claims, or that
175/// should not be injectable, opts out with `#[from_ref(skip)]`; two fields may not share a type
176/// (injection by type would be ambiguous).
177///
178/// ```ignore
179/// #[derive(FromRef)]
180/// struct AppState {
181///     orders: OrderService, // handlers can now take `State<OrderService>`
182///     #[from_ref(skip)]
183///     config: Config,
184/// }
185/// ```
186#[proc_macro_derive(FromRef, attributes(from_ref))]
187pub fn derive_from_ref(item: TokenStream) -> TokenStream {
188    let input = parse_macro_input!(item as DeriveInput);
189    from_ref::expand(&input)
190        .unwrap_or_else(syn::Error::into_compile_error)
191        .into()
192}