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
6#![forbid(unsafe_code)]
7
8mod expand;
9mod from_ref;
10mod parse;
11
12use proc_macro::TokenStream;
13use proc_macro2::TokenStream as TokenStream2;
14use quote::quote;
15use syn::{DeriveInput, ItemFn, parse_macro_input};
16
17use parse::{SubscriberArgs, doc_description};
18
19/// Turns an `async fn` handler into a mountable subscriber definition.
20///
21/// ```ignore
22/// /// Processes incoming orders.
23/// #[subscriber("orders")]
24/// async fn handle(order: &Order) -> HandlerResult { HandlerResult::Ack }
25/// // later: broker_scope.include(handle);
26///
27/// // reply form: the return value is encoded and published to "responses" through the
28/// // TypedPublisher (broker + reply codec) passed at wiring time.
29/// #[subscriber("requests", publish("responses"))]
30/// async fn reply(req: &Request) -> Response { /* ... */ }
31/// // later: broker_scope.include_publishing(reply, typed_publisher);
32///
33/// // reply form with explicit ack control: `Ok` publishes the reply, `Err` skips it and the
34/// // dispatcher acts on the returned HandlerResult.
35/// #[subscriber("requests", publish("responses"))]
36/// async fn confirm(req: &Request) -> Result<Response, HandlerResult> { /* ... */ }
37///
38/// // batch form: the handler takes the whole decoded batch as a slice; the source's
39/// // subscriber must implement BatchSubscriber. Mounted with include_batch.
40/// #[subscriber(batch("orders"))]
41/// async fn bill(orders: &[Order]) -> HandlerResult { /* settles the whole batch */ }
42///
43/// // raw form: no codec, no serde - the handler receives the payload bytes as-is. The
44/// // message parameter must be `&[u8]`; a serde-typed parameter under `raw` is an error.
45/// #[subscriber("frames", raw)]
46/// async fn on_frame(frame: &[u8]) -> HandlerResult { /* parse it yourself */ }
47///
48/// // raw reply form: the returned bytes are published as-is to "frames-out" through the bare
49/// // publisher attached at the include site (b.include(mirror).publisher(policy), or the
50/// // broker's default publish policy without the call) - no codec on either side. Returning
51/// // Result<Vec<u8>, HandlerResult> gives the same explicit ack control as the typed form.
52/// #[subscriber("frames", raw, publish_raw("frames-out"))]
53/// async fn mirror(frame: &[u8]) -> Vec<u8> { frame.to_vec() }
54///
55/// // publish_raw also composes with a typed input (the gateway shape): the input decodes with
56/// // the scope codec, the returned bytes still go out unencoded.
57/// #[subscriber("orders", publish_raw("orders-wire"))]
58/// async fn encode(order: &Order) -> Vec<u8> { /* your wire format */ }
59/// ```
60///
61/// Without `publish(..)` the handler returns any `Into<Settle>` (a `Settle`, a `HandlerResult`,
62/// `()`, or `Result<_, E>`). Attach a post-settle continuation with `HandlerResult::ack().and_after`
63/// (any outcome works), which runs after the message is settled. With `publish(..)` it returns the
64/// reply value to publish, or `Result<Reply, HandlerResult>` to control acknowledgement:
65/// `Err(result)` publishes nothing and returns `result` to the dispatcher. The `Result` form is
66/// detected syntactically, so spell it out in the signature (a type alias is treated as a plain
67/// reply type). `publish_raw(..)` is the byte reply clause: the handler returns `Vec<u8>` (any
68/// owned `AsRef<[u8]>` type) and the bytes are published unencoded through the bare publisher
69/// paired at the include site; a failed reply publish nacks the delivery with requeue, exactly
70/// like the typed reply form. It composes with both a `raw` and a typed input; `raw` with the
71/// encoded `publish(..)` is rejected (a raw handler's reply is bytes).
72///
73/// Wrapping the source in `batch(..)` switches the definition to a `BatchDef`: the handler takes
74/// `&[T]` and runs once per batch pulled from the broker's `BatchSubscriber` (use the `Buffered`
75/// adapter for brokers without native batching). It returns any `IntoBatchResult` - one outcome
76/// for the whole batch (`HandlerResult`, `()`, `Result<_, E>`), or a per-element vector
77/// (`Vec<Settle>`, or `Vec<HandlerResult>`) to settle element `i` of the slice with outcome `i`,
78/// each element carrying its own optional `and_after` continuation. The source type is recovered
79/// from the constructor path, so a generic source spells its parameters:
80/// `batch(Buffered::<Name>::new(Name::new("orders")))`.
81///
82/// Combining `batch(..)` with `publish(..)` produces a `BatchPublishingDef` (mounted with
83/// `include_batch_publishing`): the handler returns `Vec<Reply>` (or
84/// `Result<Vec<Reply>, HandlerResult>` for explicit ack control, all-or-nothing - selective
85/// outcomes do not compose with a transaction), every reply is published to the reply name, and
86/// the whole batch is acked after. Hand the mount a `TypedPublisher` for independent reply
87/// publishes, or `.transactional()` for one transaction per batch.
88///
89/// A `workers(n)` clause processes up to `n` deliveries (or batches) of this subscriber
90/// concurrently, each in its own task; global processing order is lost by design, and
91/// back-pressure holds at `n` in-flight deliveries. `workers(n, by_key)` switches to `n`
92/// sequential lanes keyed by the message's partition key, preserving per-key ordering
93/// (single-message forms only). The default is the sequential loop.
94///
95/// An `Out(out): Out<P>` parameter injects a live publisher, paired at startup from the
96/// source attached at the include site (`b.include(f).publisher(..)`); a
97/// `Seek(seeker): Seek<K>` parameter injects the subscription's own seeker, minted right
98/// after the subscription opens (the source's subscriber must implement the `Seekable`
99/// capability). The two combine freely in one handler: with each other, with `raw`, with
100/// `batch(..)`, and with every reply form (`publish(..)`, `publish_raw(..)`, and the batch
101/// publishing form). An `Out` parameter's attachment is required at the include site:
102/// `.publisher(..)` on the plain and batch forms, `.out(..)` on the reply forms (where
103/// `.publisher(..)` stays the reply's own attachment).
104///
105/// A `start_at(<position>)` clause opens the subscription at that position instead of the
106/// broker's default, seeking before the first delivery ("start from the latest on deploy",
107/// "replay the whole log"). The position is the broker's own type, named by its constructor
108/// (`MemoryPosition::start()`, a Kafka-style `latest()`); the source's subscriber must
109/// implement the `Seekable` capability, so the clause does not compile against a broker
110/// without a replayable log. The position is forced on every startup; without the clause the
111/// subscription simply opens at the broker's default, and conditional defaults stay on the
112/// broker's own subscription descriptor.
113///
114/// ```ignore
115/// // Opens at the start of the log: entries published before the service started are
116/// // replayed into the fresh subscription.
117/// #[subscriber("audit", start_at(MemoryPosition::start()))]
118/// async fn record(entry: &Entry) -> HandlerResult { /* ... */ }
119/// ```
120///
121/// In both forms the handler may declare an optional second parameter, the per-delivery
122/// `&mut Context`, to read app state or publish manually. Any further parameter is an extractor: its
123/// type must implement
124/// [`FromContext`](../ruststream/runtime/trait.FromContext.html), and the generated handler resolves
125/// it from the delivery context (in declaration order) before the body runs, so dependencies arrive
126/// as arguments. A failed extraction settles the delivery by the rejection's `HandlerResult` without
127/// running the body.
128///
129/// ```ignore
130/// // `State<Db>` is resolved from the application state before the body runs.
131/// #[subscriber("orders")]
132/// async fn handle(order: &Order, State(db): State<Db>) -> HandlerResult { /* ... */ }
133/// ```
134#[proc_macro_attribute]
135pub fn subscriber(attr: TokenStream, item: TokenStream) -> TokenStream {
136    let args = parse_macro_input!(attr as SubscriberArgs);
137    let func = parse_macro_input!(item as ItemFn);
138    expand::subscriber(&args, &func).unwrap_or_else(|err| err.to_compile_error().into())
139}
140
141/// Generates a `main` entry point for a `RustStream` service.
142///
143/// Place it on a synchronous, argument-free function that builds and returns an application -
144/// `impl App` (the recommended form, hiding the composed type parameters) or a concrete
145/// `RustStream<_>`. The expansion keeps the function and adds a `main` that hands it to
146/// `ruststream::runtime::cli::run_main`, producing a binary that understands the `run` and
147/// `asyncapi gen` commands with no hand-written runtime boilerplate.
148///
149/// ```ignore
150/// #[ruststream::app]
151/// fn app() -> impl App {
152///     RustStream::new(AppInfo::new("svc", "0.1.0")).register_broker(MemoryBroker::new())
153/// }
154/// ```
155#[proc_macro_attribute]
156pub fn app(attr: TokenStream, item: TokenStream) -> TokenStream {
157    let func = parse_macro_input!(item as ItemFn);
158    expand_app(&attr.into(), &func).unwrap_or_else(|err| err.to_compile_error().into())
159}
160
161fn expand_app(attr: &TokenStream2, func: &ItemFn) -> syn::Result<TokenStream> {
162    if !attr.is_empty() {
163        return Err(syn::Error::new_spanned(
164            attr,
165            "#[ruststream::app] takes no arguments",
166        ));
167    }
168    if let Some(asyncness) = func.sig.asyncness {
169        return Err(syn::Error::new_spanned(
170            asyncness,
171            "#[ruststream::app] requires a synchronous builder returning `impl App` or `RustStream`",
172        ));
173    }
174    if !func.sig.inputs.is_empty() {
175        return Err(syn::Error::new_spanned(
176            &func.sig.inputs,
177            "#[ruststream::app] builder must take no arguments",
178        ));
179    }
180    let name = &func.sig.ident;
181    Ok(quote! {
182        #func
183
184        fn main() -> ::std::process::ExitCode {
185            ::ruststream::runtime::cli::run_main(#name)
186        }
187    }
188    .into())
189}
190
191/// Derives [`Message`](../ruststream/trait.Message.html) metadata: the type name and its doc
192/// comment.
193///
194/// ```ignore
195/// /// An order placed by a customer.
196/// #[derive(Message)]
197/// struct Order { id: u32 }
198/// // Order::NAME == "Order", Order::DESCRIPTION == Some("An order placed by a customer.")
199/// ```
200#[proc_macro_derive(Message)]
201pub fn derive_message(item: TokenStream) -> TokenStream {
202    let input = parse_macro_input!(item as DeriveInput);
203    let name = &input.ident;
204    let name_str = name.to_string();
205    let description = doc_description(&input.attrs);
206    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
207
208    quote! {
209        impl #impl_generics ::ruststream::Message for #name #ty_generics #where_clause {
210            const NAME: &'static str = #name_str;
211            const DESCRIPTION: ::core::option::Option<&'static str> = #description;
212        }
213    }
214    .into()
215}
216
217/// Derives [`FromRef`](../ruststream/runtime/trait.FromRef.html) for each field of an
218/// application-state struct, so `#[subscriber]` handlers can inject any field with
219/// `State<FieldType>` without a hand-written impl.
220///
221/// Each field gets a `FromRef` impl that clones it out of the state. Because the generated impl
222/// carries no generic parameter, it is legal even for fields whose type comes from another crate (a
223/// broker publisher, a client pool). A field that another field's type already claims, or that
224/// should not be injectable, opts out with `#[from_ref(skip)]`; two fields may not share a type
225/// (injection by type would be ambiguous).
226///
227/// ```ignore
228/// #[derive(FromRef)]
229/// struct AppState {
230///     orders: OrderService, // handlers can now take `State<OrderService>`
231///     #[from_ref(skip)]
232///     config: Config,
233/// }
234/// ```
235#[proc_macro_derive(FromRef, attributes(from_ref))]
236pub fn derive_from_ref(item: TokenStream) -> TokenStream {
237    let input = parse_macro_input!(item as DeriveInput);
238    from_ref::expand(&input)
239        .unwrap_or_else(syn::Error::into_compile_error)
240        .into()
241}