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 lanes;
11mod outgoing;
12mod parse;
13
14use proc_macro::TokenStream;
15use proc_macro2::TokenStream as TokenStream2;
16use quote::{ToTokens, quote};
17use syn::punctuated::Punctuated;
18use syn::{Attribute, DeriveInput, Ident, ItemFn, Meta, Token, Type, parse_macro_input};
19
20use parse::{SubscriberArgs, doc_description};
21
22/// Turns an `async fn` handler into a mountable subscriber definition.
23///
24/// The attribute carries the declarative settings of the subscription; the *shape* of the
25/// handler - one message (`&T`) or a whole batch of them (`&[T]`) - is read off the signature,
26/// which is the only place it was ever really written. Which lane a type rides is the type's
27/// own business: a `serde` type decodes through the scope codec, a `#[derive(Deserialized)]`
28/// type constructs itself from the payload bytes, and a reply encodes through the reply codec
29/// unless its type is `#[derive(Serialized)]`, in which case its bytes leave as they are.
30///
31/// ```ignore
32/// /// Processes incoming orders.
33/// #[subscriber("orders")]
34/// async fn handle(order: &Order) -> HandlerOutcome { HandlerOutcome::ack() }
35/// // later: broker_scope.include(handle);
36///
37/// // reply form: the return value is encoded and published to "responses" through the
38/// // reply publisher attached at the include site (the broker's default without it).
39/// #[subscriber("requests", publish("responses"))]
40/// async fn reply(req: &Request) -> Response { /* ... */ }
41///
42/// // reply form with explicit ack control: `Ok` publishes the reply, `Err` skips it and the
43/// // dispatcher settles by the returned HandlerOutcome.
44/// #[subscriber("requests", publish("responses"))]
45/// async fn confirm(req: &Request) -> Result<Response, HandlerOutcome> { /* ... */ }
46///
47/// // batch form: the slice parameter says the handler takes the whole decoded batch; the
48/// // subscription's subscriber must implement BatchSubscriber (natively, or through the
49/// // buffer the mount site adds).
50/// #[subscriber("orders")]
51/// async fn bill(orders: &[Order]) -> HandlerOutcome { /* settles the whole batch */ }
52///
53/// // raw form: no codec, no serde - the input type deserializes itself from the payload
54/// // bytes. `Deserialize` means the framework's codec does it; `Deserialized` means it is
55/// // already done by the user's own type.
56/// #[derive(Deserialized)]
57/// struct Frame<'a>(&'a [u8]);
58///
59/// #[subscriber("frames")]
60/// async fn on_frame(frame: &Frame<'_>) -> HandlerOutcome { /* parse it yourself */ }
61///
62/// // raw batch: the batch shape on the same lane; the views borrow the batch's own messages
63/// // for the duration of the call.
64/// #[subscriber("frames")]
65/// async fn ingest(frames: &[Frame<'_>]) -> HandlerOutcome { /* parse them yourself */ }
66///
67/// // the settings the attribute leaves out are filled in at the mount site, through the same
68/// // builder the attribute expands into:
69/// #[subscriber]
70/// async fn audit(order: &Order) -> HandlerOutcome { HandlerOutcome::ack() }
71/// // later: broker_scope.include(audit.name(subject).workers(nonzero!(4)));
72///
73/// // raw reply: the reply type carries its own bytes, so they are published as they are -
74/// // no codec on the way out. `Serialize` means the framework's codec does it; `Serialized`
75/// // means it is already done by the user's own type. The same publish("dest") clause serves
76/// // both wires, and Result<Export, HandlerOutcome> gives the usual explicit ack control.
77/// #[derive(Serialized)]
78/// struct Export(Vec<u8>);
79///
80/// #[subscriber("frames", publish("frames-out"))]
81/// async fn mirror(frame: &Frame<'_>) -> Export { Export(frame.0.to_vec()) }
82///
83/// // the lanes mix freely (the gateway shape): the input decodes with the scope codec, the
84/// // returned bytes still go out unencoded - and the other diagonal (a Deserialized input
85/// // with an encoded serde reply) works the same way.
86/// #[subscriber("orders", publish("orders-wire"))]
87/// async fn encode(order: &Order) -> Export { /* your wire format */ }
88/// ```
89///
90/// Without `publish(..)` the handler returns any accepted outcome shape (a `HandlerOutcome`,
91/// `()`, or `Result<_, E>`). Attach a post-settle continuation with
92/// `HandlerOutcome::ack().and_after` (any outcome works), which runs after the message is
93/// settled. With `publish(..)` it returns the reply value to publish, or
94/// `Result<Reply, HandlerOutcome>` to control acknowledgement: `Err(outcome)` publishes nothing
95/// and settles by `outcome`. The `Result` form is
96/// detected syntactically, so spell it out in the signature (a type alias is treated as a plain
97/// reply type). A failed reply publish nacks the delivery with requeue on both wires, so a
98/// reply is retried rather than silently lost.
99///
100/// A `&[T]` message parameter makes the definition a batch one: the handler runs once per batch
101/// pulled from the subscription's `BatchSubscriber` (added by the mount site's buffer for
102/// subscriptions without native batching). It returns one outcome
103/// for the whole batch (`HandlerOutcome`, `()`, `Result<_, E>`), or a per-element
104/// `Vec<HandlerOutcome>` to settle element `i` of the slice with outcome `i`, each element
105/// carrying its own optional `and_after` continuation. A `&[Message<H, T>]` parameter pairs
106/// each element with its typed
107/// header contract: the core decodes both under the subscriber's decode policy, and the body
108/// reads `element.headers` next to `element.body` (the single-message counterpart stays the
109/// `Headers<T>` extractor parameter). A batch body may name the broker's subscription-scoped
110/// batch context (`ctx: &mut Context<'_, MemoryBatchContext>`-style) to reach handles every
111/// delivery of the subscription shares, like a seeker.
112///
113/// Combining a batch handler with `publish(..)` produces a batch reply definition: the handler
114/// returns `Vec<Reply>` (or
115/// `Result<Vec<Reply>, HandlerOutcome>` for explicit ack control, all-or-nothing - selective
116/// outcomes do not compose with a transaction), every reply is published to the reply name, and
117/// the whole batch is acked after. Name the reply's policy with `.out(Reply, ..)` at the mount
118/// site for independent reply publishes, and chain `.transactional()` for one transaction per
119/// batch.
120///
121/// A `workers(n)` clause processes up to `n` deliveries (or batches) of this subscriber
122/// concurrently, each in its own task; global processing order is lost by design, and
123/// back-pressure holds at `n` in-flight deliveries. `workers(n, by_key)` switches to `n`
124/// sequential lanes keyed by the message's partition key, preserving per-key ordering
125/// (single-message forms only). The default is the sequential loop.
126///
127/// Clause values need not be literals: `workers(..)` takes any `usize` expression (a constant,
128/// a static, a function call - an integer literal keeps the compile-time zero rejection, a
129/// runtime value of zero panics at registration), `publish(..)` / `publish_raw(..)` take a
130/// `&'static str` expression, and `on_failure(..)` keys accept a `FailurePolicy` expression next
131/// to the keyword vocabulary.
132///
133/// An `Out(out): Out<P>` parameter injects a live publisher, paired at startup from the
134/// policy attached at the include site (`b.include(f).out(marker, policy).build()`); its optional third
135/// position declares the message set the handler publishes (`Out<impl Publisher, Marker,
136/// (A, B)>` - a tuple, a single type, or a `#[derive(OutMessages)]` set enum), narrowing what
137/// the publish builder accepts on it, and the generated document reports that narrowed list as
138/// the handler's send operations. `Out` parameters combine freely in one handler: with each
139/// other, with a byte input, with a batch handler, and with every reply form (`publish(..)`,
140/// `publish_raw(..)`, and the batch publishing form). An `Out` parameter's attachment is
141/// required at the include site: one `.out(marker, policy)` per slot, next to the reply's own
142/// optional `.out(Reply, policy)`, and a `.build()` to commit. Repositioning a
143/// subscription from inside the body is not a parameter but a broker context field: the broker
144/// publishes its position and seek-handle keys, and the handler reads them with the `Ctx`
145/// extractor like any other broker field.
146///
147/// A `start_at(<position>)` clause opens the subscription at that position instead of the
148/// broker's default, seeking before the first delivery ("start from the latest on deploy",
149/// "replay the whole log"). The position is the broker's own type, named by its constructor
150/// (`MemoryPosition::start()`, a Kafka-style `latest()`); the source's subscriber must
151/// implement the `Seekable` capability, so the clause does not compile against a broker
152/// without a replayable log. The position is forced on every startup; without the clause the
153/// subscription simply opens at the broker's default, and conditional defaults stay on the
154/// broker's own subscription descriptor.
155///
156/// ```ignore
157/// // Opens at the start of the log: entries published before the service started are
158/// // replayed into the fresh subscription.
159/// #[subscriber("audit", start_at(MemoryPosition::start()))]
160/// async fn record(entry: &Entry) -> HandlerOutcome { /* ... */ }
161/// ```
162///
163/// In both forms the handler may declare an optional second parameter, the per-delivery
164/// `&mut Context`, to read app state or publish manually. Any further parameter is an extractor: its
165/// type must implement
166/// [`FromContext`](../ruststream/runtime/trait.FromContext.html), and the generated handler resolves
167/// it from the delivery context (in declaration order) before the body runs, so dependencies arrive
168/// as arguments. A failed extraction settles the delivery by the rejection's `HandlerOutcome`
169/// without running the body.
170///
171/// ```ignore
172/// // `State<Db>` is resolved from the application state before the body runs.
173/// #[subscriber("orders")]
174/// async fn handle(order: &Order, State(db): State<Db>) -> HandlerOutcome { /* ... */ }
175/// ```
176///
177/// The attribute expands into the definition and then the settings builder over it, so the
178/// clauses above are exactly the calls a user would write at the mount site (see
179/// `SubscriberSettings` in the core crate). A setting the attribute names is fixed in the
180/// definition's type and its builder method no longer applies, so the two can never disagree.
181#[proc_macro_attribute]
182pub fn subscriber(attr: TokenStream, item: TokenStream) -> TokenStream {
183    let args = parse_macro_input!(attr as SubscriberArgs);
184    let func = parse_macro_input!(item as ItemFn);
185    expand::subscriber(&args, &func).unwrap_or_else(|err| err.to_compile_error().into())
186}
187
188/// Generates a `main` entry point for a `RustStream` service.
189///
190/// Place it on a synchronous, argument-free function that builds and returns an application -
191/// `impl App` (the recommended form, hiding the composed type parameters) or a concrete
192/// `RustStream<_>`. The expansion keeps the function and adds a `main` that hands it to
193/// `ruststream::runtime::cli::run_main`, producing a binary that understands the `run` and
194/// `asyncapi gen` commands with no hand-written runtime boilerplate.
195///
196/// ```ignore
197/// #[ruststream::app]
198/// fn app() -> impl App {
199///     RustStream::new(AppInfo::new("svc", "0.1.0")).register_broker(MemoryBroker::new())
200/// }
201/// ```
202#[proc_macro_attribute]
203pub fn app(attr: TokenStream, item: TokenStream) -> TokenStream {
204    let func = parse_macro_input!(item as ItemFn);
205    expand_app(&attr.into(), &func).unwrap_or_else(|err| err.to_compile_error().into())
206}
207
208fn expand_app(attr: &TokenStream2, func: &ItemFn) -> syn::Result<TokenStream> {
209    if !attr.is_empty() {
210        return Err(syn::Error::new_spanned(
211            attr,
212            "#[ruststream::app] takes no arguments",
213        ));
214    }
215    if let Some(asyncness) = func.sig.asyncness {
216        return Err(syn::Error::new_spanned(
217            asyncness,
218            "#[ruststream::app] requires a synchronous builder returning `impl App` or `RustStream`",
219        ));
220    }
221    if !func.sig.inputs.is_empty() {
222        return Err(syn::Error::new_spanned(
223            &func.sig.inputs,
224            "#[ruststream::app] builder must take no arguments",
225        ));
226    }
227    let name = &func.sig.ident;
228    Ok(quote! {
229        #func
230
231        fn main() -> ::std::process::ExitCode {
232            ::ruststream::runtime::cli::run_main(#name)
233        }
234    }
235    .into())
236}
237
238/// Derives [`Message`](../ruststream/trait.Message.html) metadata: the type name, its doc
239/// comment, and its header contract.
240///
241/// The optional `#[message(headers(Meta))]` attribute declares the type's typed header
242/// contract (see `MessageHeaders` in the core crate): `Meta` is serialized into the header map
243/// on the typed publish path and lifted into the message's `AsyncAPI` headers schema. Without
244/// the attribute the type declares no typed headers.
245///
246/// The derive describes a message the service receives, or replies with. A message the handler
247/// sends through an `Out` slot declares where it goes as well, so it derives
248/// [`Outgoing`](derive.Outgoing.html) instead - only that derive makes the type a declared
249/// message set (see `OutMessages` in the core crate) the third `Out` argument can name.
250///
251/// ```ignore
252/// /// An order placed by a customer.
253/// #[derive(MessageInfo)]
254/// struct Order { id: u32 }
255/// // Order::NAME == "Order", Order::DESCRIPTION == Some("An order placed by a customer.")
256///
257/// #[derive(Message, Serialize)]
258/// #[message(headers(ChunkMeta))]
259/// struct ChunkDone { output_key: String }
260/// ```
261#[proc_macro_derive(MessageInfo, attributes(message))]
262pub fn derive_message(item: TokenStream) -> TokenStream {
263    let input = parse_macro_input!(item as DeriveInput);
264    let name = &input.ident;
265    let name_str = name.to_string();
266    let description = doc_description(&input.attrs);
267    let contract = match message_headers_contract(&input.attrs) {
268        Ok(Some(headers)) => quote!(::ruststream::WithHeaders<#headers>),
269        Ok(None) => quote!(::ruststream::NoHeaders),
270        Err(err) => return err.into_compile_error().into(),
271    };
272    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
273
274    quote! {
275        impl #impl_generics ::ruststream::MessageInfo for #name #ty_generics #where_clause {
276            const NAME: &'static str = #name_str;
277            const DESCRIPTION: ::core::option::Option<&'static str> = #description;
278        }
279
280        impl #impl_generics ::ruststream::MessageHeaders for #name #ty_generics #where_clause {
281            type Contract = #contract;
282        }
283    }
284    .into()
285}
286
287/// Derives everything a message type declares about being sent, from one `key = value`
288/// attribute: its destination
289/// ([`OutgoingDestination`](../ruststream/trait.OutgoingDestination.html)), its optional typed
290/// header contract, and the [`Message`](../ruststream/trait.Message.html) metadata the generated
291/// document reads (the type's name and doc comment).
292///
293/// The destination decides which positions the publish builder demands at the call site:
294///
295/// * `#[outgoing(name = "orders.done")]` fixes it - `publisher.message(&done).publish()` names
296///   nothing.
297/// * `#[outgoing(name = "orders.{tenant}.v1")]` declares a space of names - `to()` opens one
298///   setter per placeholder, and the publish appears once the last one is bound.
299/// * the derive alone declares nothing - the call site names it: `.to("orders.archived")`.
300///
301/// `headers = Meta` declares the typed header contract: `Meta` stays an ordinary serde struct
302/// the derive does not touch, and the publish builder then demands `with_headers(&meta)`.
303///
304/// Do not combine with `#[derive(MessageInfo)]` on the same type: this derive already provides the
305/// message metadata, and the two produce conflicting impls.
306///
307/// ```ignore
308/// /// A finished order.
309/// #[derive(Outgoing, Serialize)]
310/// #[outgoing(name = "orders.done", headers = OrderMeta)]
311/// struct OrderDone { id: u64 }
312///
313/// #[derive(Outgoing, Serialize)]
314/// #[outgoing(name = "orders.{tenant}.{region}.v1")]
315/// struct OrderPlaced { id: u64 }
316/// // out.message(&placed).to().tenant("acme").region("eu").publish().await?;
317///
318/// #[derive(Outgoing, Serialize)]
319/// struct OrderArchived { id: u64 }
320/// // out.message(&archived).to("orders.archived").publish().await?;
321/// ```
322#[proc_macro_derive(Outgoing, attributes(outgoing))]
323pub fn derive_outgoing(item: TokenStream) -> TokenStream {
324    let input = parse_macro_input!(item as DeriveInput);
325    outgoing::expand(&input)
326        .unwrap_or_else(syn::Error::into_compile_error)
327        .into()
328}
329
330/// Parses the optional `#[message(headers(Type))]` helper attribute of `#[derive(MessageInfo)]`.
331fn message_headers_contract(attrs: &[Attribute]) -> syn::Result<Option<Type>> {
332    let mut found: Option<Type> = None;
333    for attr in attrs.iter().filter(|attr| attr.path().is_ident("message")) {
334        let meta: Meta = attr.parse_args()?;
335        let Meta::List(list) = &meta else {
336            return Err(syn::Error::new_spanned(
337                meta,
338                "expected `headers(<Type>)` inside #[message(..)]",
339            ));
340        };
341        if !list.path.is_ident("headers") {
342            return Err(syn::Error::new_spanned(
343                &list.path,
344                "expected `headers(<Type>)` inside #[message(..)]",
345            ));
346        }
347        let headers: Type = list.parse_args()?;
348        if found.replace(headers).is_some() {
349            return Err(syn::Error::new_spanned(
350                attr,
351                "duplicate `headers(..)`: a message declares one header contract",
352            ));
353        }
354    }
355    Ok(found)
356}
357
358/// Derives [`FromRef`](../ruststream/runtime/trait.FromRef.html) for each field of an
359/// application-state struct, so `#[subscriber]` handlers can inject any field with
360/// `State<FieldType>` without a hand-written impl.
361///
362/// Each field gets a `FromRef` impl that clones it out of the state. Because the generated impl
363/// carries no generic parameter, it is legal even for fields whose type comes from another crate (a
364/// broker publisher, a client pool). A field that another field's type already claims, or that
365/// should not be injectable, opts out with `#[from_ref(skip)]`; two fields may not share a type
366/// (injection by type would be ambiguous).
367///
368/// ```ignore
369/// #[derive(FromRef)]
370/// struct AppState {
371///     orders: OrderService, // handlers can now take `State<OrderService>`
372///     #[from_ref(skip)]
373///     config: Config,
374/// }
375/// ```
376#[proc_macro_derive(FromRef, attributes(from_ref))]
377pub fn derive_from_ref(item: TokenStream) -> TokenStream {
378    let input = parse_macro_input!(item as DeriveInput);
379    from_ref::expand(&input)
380        .unwrap_or_else(syn::Error::into_compile_error)
381        .into()
382}
383
384/// Derives [`OutSlot`](../ruststream/runtime/trait.OutSlot.html) for a unit struct, making it a
385/// slot marker for `Out<impl Publisher, Marker>` handler parameters. The marker's
386/// [`NAME`](../ruststream/runtime/trait.OutSlot.html#associatedconstant.NAME) is the struct's
387/// identifier, used by startup errors and test assertions.
388///
389/// The optional `#[publishes(Type, ..)]` attribute lists the message types the slot may
390/// publish, which is what the generated document reports for a handler leaving its `Out`
391/// parameter unrestricted. The list is enforced, not only documented: publishing a type it does
392/// not name is a compile error (see `PublishedThrough` in the core crate), so the document cannot
393/// fall behind what handlers send. Each listed type declares its own destination with
394/// `#[derive(Outgoing)]`; listing one type twice is an error.
395///
396/// ```ignore
397/// #[derive(OutSlot)]
398/// struct Encoded;
399///
400/// #[derive(OutSlot)]
401/// #[publishes(ChunkDone, Progress)]
402/// struct Events;
403///
404/// #[subscriber("chunks")]
405/// async fn transcode(chunk: &[u8], Out(out): Out<impl Publisher, Encoded>) -> HandlerOutcome {
406///     /* ... */
407/// }
408/// // include site: b.include(transcode).out(Encoded, KafkaPublish::default());
409/// ```
410#[proc_macro_derive(OutSlot, attributes(publishes))]
411pub fn derive_out_slot(item: TokenStream) -> TokenStream {
412    let input = parse_macro_input!(item as DeriveInput);
413    let syn::Data::Struct(data) = &input.data else {
414        return syn::Error::new_spanned(&input.ident, "OutSlot is derived on a unit struct")
415            .into_compile_error()
416            .into();
417    };
418    if !matches!(data.fields, syn::Fields::Unit) {
419        return syn::Error::new_spanned(
420            &data.fields,
421            "an OutSlot marker is a unit struct: it identifies the slot and carries no data",
422        )
423        .into_compile_error()
424        .into();
425    }
426    if !input.generics.params.is_empty() {
427        return syn::Error::new_spanned(
428            &input.generics,
429            "an OutSlot marker takes no generic parameters",
430        )
431        .into_compile_error()
432        .into();
433    }
434    let dictionary = match publishes_dictionary(&input.attrs) {
435        Ok(dictionary) => dictionary,
436        Err(err) => return err.into_compile_error().into(),
437    };
438    let name = &input.ident;
439    let name_str = name.to_string();
440
441    let outgoing = slot_outgoing_metadata(name, &dictionary);
442    // The list is the whole membership: what the document reports as leaving the slot is exactly
443    // what the publish builder admits.
444    let memberships = dictionary.iter().map(|ty| {
445        quote! {
446            impl ::ruststream::runtime::PublishedThrough<#name> for #ty {}
447        }
448    });
449
450    quote! {
451        impl ::ruststream::runtime::OutSlot for #name {
452            const NAME: &'static str = #name_str;
453
454            #outgoing
455        }
456
457        #(#memberships)*
458    }
459    .into()
460}
461
462/// The marker's `outgoing()` override, reporting its `#[publishes(..)]` list as the document's
463/// outgoing messages: each listed type contributes what its own `#[derive(Outgoing)]` declares.
464/// An empty list keeps the trait's own default (nothing declared).
465fn slot_outgoing_metadata(name: &Ident, dictionary: &[Type]) -> TokenStream2 {
466    if dictionary.is_empty() {
467        return quote!();
468    }
469    let entries = dictionary.iter().map(|ty| {
470        quote! {
471            __rs_entries.extend(
472                <#ty as ::ruststream::runtime::OutMessages<#name>>::outgoing(),
473            );
474        }
475    });
476    quote! {
477        fn outgoing() -> ::std::vec::Vec<::ruststream::runtime::OutgoingMessageMetadata> {
478            let mut __rs_entries = ::std::vec::Vec::new();
479            #(#entries)*
480            __rs_entries
481        }
482    }
483}
484
485/// Derives a declared message set (`OutMessages` in the core crate) from an enum whose
486/// variants each wrap one message model. The enum is a type-level declaration: it is never
487/// constructed, its variants' inner types are the set.
488///
489/// Naming the enum as the third `Out` argument (`Out<impl Publisher, Marker, SendSet>`)
490/// declares that the handler publishes exactly those models: the publish builder compiles for
491/// each of them (and for nothing else), and the generated `AsyncAPI` document lists them as the
492/// handler's `send` operations. Each model declares where it goes with `#[derive(Outgoing)]`.
493///
494/// Do not combine with `#[derive(Outgoing)]` on the same enum: a type is a message or a set,
495/// and the two derives produce conflicting impls.
496///
497/// ```ignore
498/// #[derive(OutMessages)]
499/// enum ConvertSends {
500///     Progress(Progress),
501///     Done(ChunkDone),
502/// }
503///
504/// #[subscriber("chunks.raw")]
505/// async fn convert(
506///     chunk: &Chunk,
507///     Out(out): Out<impl Publisher, Events, ConvertSends>,
508/// ) -> HandlerOutcome { /* out.message(&Progress { .. }).publish() */ }
509/// ```
510#[proc_macro_derive(OutMessages)]
511pub fn derive_out_messages(item: TokenStream) -> TokenStream {
512    let input = parse_macro_input!(item as DeriveInput);
513    let syn::Data::Enum(data) = &input.data else {
514        return syn::Error::new_spanned(
515            &input.ident,
516            "OutMessages is derived on an enum whose variants each wrap one message model",
517        )
518        .into_compile_error()
519        .into();
520    };
521    if !input.generics.params.is_empty() {
522        return syn::Error::new_spanned(
523            &input.generics,
524            "an OutMessages set takes no generic parameters",
525        )
526        .into_compile_error()
527        .into();
528    }
529    let models = match out_messages_models(data) {
530        Ok(models) => models,
531        Err(err) => return err.into_compile_error().into(),
532    };
533    let name = &input.ident;
534    let memberships = models.iter().enumerate().map(|(index, model)| {
535        quote! {
536            impl ::ruststream::runtime::ContainsMessage<
537                #model,
538                ::ruststream::runtime::SlotPos<#index>,
539            > for #name
540            {
541            }
542        }
543    });
544    // Each model already declares itself as a one-element set; the enum's contribution is the
545    // concatenation, so a model's destination is described in exactly one place.
546    let entries = models.iter().map(|model| {
547        quote! {
548            __rs_entries.extend(
549                <#model as ::ruststream::runtime::OutMessages<__RsM>>::outgoing(),
550            );
551        }
552    });
553    quote! {
554        #(#memberships)*
555
556        impl<__RsM: ::ruststream::runtime::OutSlot> ::ruststream::runtime::OutMessages<__RsM>
557            for #name
558        where
559            #(#models: ::ruststream::runtime::OutMessages<__RsM>,)*
560        {
561            fn outgoing()
562                -> ::std::vec::Vec<::ruststream::runtime::OutgoingMessageMetadata>
563            {
564                let mut __rs_entries = ::std::vec::Vec::new();
565                #(#entries)*
566                __rs_entries
567            }
568        }
569    }
570    .into()
571}
572
573/// Derives a self-deserializing input type (`Deserialized` in the core crate): `Deserialize`
574/// means the framework's codec does it, `Deserialized` means it is already done by the user's
575/// own type.
576///
577/// Covers the obvious shape - a newtype or single-field struct over the payload view
578/// `&'a [u8]` - and emits the construction plus both `Input` spellings, so `&Frame<'_>` and
579/// `&[Frame<'_>]` bodies mount.
580///
581/// A type that deserializes *itself* - a generated Protobuf message, a flatbuffers root, a capnp
582/// reader - names its format's own reader with `#[wire(decode = <path>)]` instead, and then the
583/// shape of the type is its own business. The path is a function `fn(&[u8]) -> Result<Self, E>`
584/// (or `fn(&'a [u8]) -> Result<Self<'a>, E>` for a borrowing view); the derive calls it and
585/// nothing here depends on the format's crate. `#[wire(prost)]` is the shorthand for
586/// `prost::Message::decode`.
587///
588/// ```ignore
589/// #[derive(Deserialized)]
590/// struct Frame<'a>(&'a [u8]);
591///
592/// #[subscriber("frames")]
593/// async fn inspect(frame: &Frame<'_>) -> HandlerOutcome { /* parse it yourself */ }
594///
595/// // What a generator's config puts on every message it emits.
596/// #[derive(Clone, PartialEq, prost::Message, Deserialized)]
597/// #[wire(prost)]
598/// struct Order { #[prost(uint64, tag = "1")] id: u64 }
599/// ```
600#[proc_macro_derive(Deserialized, attributes(wire))]
601pub fn derive_deserialized(item: TokenStream) -> TokenStream {
602    let input = parse_macro_input!(item as DeriveInput);
603    lanes::derive_deserialized(&input)
604        .unwrap_or_else(syn::Error::into_compile_error)
605        .into()
606}
607
608/// Derives a self-serialized outgoing type (`Serialized` in the core crate): `Serialize` means
609/// the framework's codec does it, `Serialized` means it is already done by the user's own
610/// type.
611///
612/// Covers the obvious shape - a newtype or single-field struct over an owned byte buffer - and
613/// emits the bytes accessor plus the wire spellings that route the type onto the serialized
614/// wire (`MessageWire` for a typed publish, `ReplyShape` for the reply position), so a handler
615/// returning it - or passing it to `message(..)` - publishes those bytes as they are.
616///
617/// A type that serializes *itself* names its format's own writer with
618/// `#[wire(encode = <path>)]`, and then holds fields rather than bytes: the path is a function
619/// `fn(&Self, &mut impl BufMut)`, returning either nothing or `Result<(), E>`, and the derive
620/// calls it against the buffer the publish path already carries - so the value is encoded once,
621/// into the buffer that leaves. Nothing here depends on the format's crate. `#[wire(prost)]` is
622/// the shorthand for `prost::Message::encode`.
623///
624/// ```ignore
625/// #[derive(Serialized)]
626/// struct Export(Vec<u8>);
627///
628/// #[subscriber("orders", publish("orders-wire"))]
629/// async fn encode(order: &Order) -> Export { /* your wire format */ }
630///
631/// // What a generator's config puts on every message it emits.
632/// #[derive(Clone, PartialEq, prost::Message, Serialized)]
633/// #[wire(prost)]
634/// struct OrderDone { #[prost(uint64, tag = "1")] id: u64 }
635/// ```
636#[proc_macro_derive(Serialized, attributes(wire))]
637pub fn derive_serialized(item: TokenStream) -> TokenStream {
638    let input = parse_macro_input!(item as DeriveInput);
639    lanes::derive_serialized(&input)
640        .unwrap_or_else(syn::Error::into_compile_error)
641        .into()
642}
643
644/// The set enum's models, one per variant: each variant wraps exactly one type, and a
645/// duplicate model is rejected (its membership index inference would be ambiguous).
646fn out_messages_models(data: &syn::DataEnum) -> syn::Result<Vec<&Type>> {
647    let mut models: Vec<&Type> = Vec::new();
648    for variant in &data.variants {
649        let syn::Fields::Unnamed(fields) = &variant.fields else {
650            return Err(syn::Error::new_spanned(
651                variant,
652                "an OutMessages variant wraps exactly one message model: `Variant(Model)`",
653            ));
654        };
655        if fields.unnamed.len() != 1 {
656            return Err(syn::Error::new_spanned(
657                fields,
658                "an OutMessages variant wraps exactly one message model: `Variant(Model)`",
659            ));
660        }
661        let model = &fields.unnamed[0].ty;
662        let rendered = model.to_token_stream().to_string();
663        if models
664            .iter()
665            .any(|existing| existing.to_token_stream().to_string() == rendered)
666        {
667            return Err(syn::Error::new_spanned(
668                model,
669                "this model is already in the set: a duplicate would make the membership \
670                 index inference ambiguous",
671            ));
672        }
673        models.push(model);
674    }
675    Ok(models)
676}
677
678/// Parses every `#[publishes(Type, ..)]` attribute into one list, rejecting a type listed twice
679/// (a second entry would say nothing the first does not).
680fn publishes_dictionary(attrs: &[Attribute]) -> syn::Result<Vec<Type>> {
681    let mut dictionary: Vec<Type> = Vec::new();
682    for attr in attrs
683        .iter()
684        .filter(|attr| attr.path().is_ident("publishes"))
685    {
686        let entries = attr.parse_args_with(Punctuated::<Type, Token![,]>::parse_terminated)?;
687        for ty in entries {
688            let name = ty.to_token_stream().to_string();
689            if dictionary
690                .iter()
691                .any(|existing| existing.to_token_stream().to_string() == name)
692            {
693                return Err(syn::Error::new_spanned(
694                    &ty,
695                    "this type is already listed on the slot: a type appears once",
696                ));
697            }
698            dictionary.push(ty);
699        }
700    }
701    Ok(dictionary)
702}