Skip to main content

angzarr_macros/
lib.rs

1//! Procedural macros for angzarr OO-style component definitions.
2//!
3//! # Aggregate Example
4//!
5//! ```rust,ignore
6//! use angzarr_macros::{aggregate, handles, applies, rejected};
7//!
8//! #[aggregate(domain = "player")]
9//! impl PlayerAggregate {
10//!     type State = PlayerState;
11//!
12//!     #[applies(PlayerRegistered)]
13//!     fn apply_registered(state: &mut PlayerState, event: PlayerRegistered) {
14//!         state.player_id = format!("player_{}", event.email);
15//!         state.display_name = event.display_name;
16//!         state.exists = true;
17//!     }
18//!
19//!     #[applies(FundsDeposited)]
20//!     fn apply_deposited(state: &mut PlayerState, event: FundsDeposited) {
21//!         if let Some(balance) = event.new_balance {
22//!             state.bankroll = balance.amount;
23//!         }
24//!     }
25//!
26//!     #[handles(RegisterPlayer)]
27//!     fn register(&self, cb: &CommandBook, cmd: RegisterPlayer, state: &PlayerState, seq: u32)
28//!         -> CommandResult<EventBook> {
29//!         // ...
30//!     }
31//!
32//!     #[rejected(domain = "payment", command = "ProcessPayment")]
33//!     fn handle_payment_rejected(&self, notification: &Notification, state: &PlayerState)
34//!         -> CommandResult<BusinessResponse> {
35//!         // ...
36//!     }
37//! }
38//! ```
39//!
40//! # Saga Example
41//!
42//! ```rust,ignore
43//! use angzarr_macros::{saga, handles};
44//!
45//! #[saga(name = "saga-order-fulfillment", input = "order")]
46//! impl OrderFulfillmentSaga {
47//!     #[handles(OrderCompleted)]
48//!     fn handle_completed(&self, event: OrderCompleted, source: &EventBook)
49//!         -> CommandResult<SagaHandlerResponse> {
50//!         // ...
51//!     }
52//! }
53//! ```
54
55use proc_macro::TokenStream;
56use proc_macro2::TokenStream as TokenStream2;
57use quote::{quote, ToTokens};
58use syn::{
59    parse_macro_input, Attribute, Ident, ImplItem, ItemImpl, Meta, Token,
60};
61
62/// Marks an impl block as an aggregate with command handlers.
63///
64/// # Attributes
65/// - `domain = "name"` - The aggregate's domain name (required)
66///
67/// # Example
68/// ```rust,ignore
69/// #[aggregate(domain = "player")]
70/// impl PlayerAggregate {
71///     #[handles(RegisterPlayer)]
72///     fn register(&self, cmd: RegisterPlayer, state: &PlayerState, seq: u32)
73///         -> CommandResult<EventBook> {
74///         // ...
75///     }
76/// }
77/// ```
78#[proc_macro_attribute]
79pub fn aggregate(attr: TokenStream, item: TokenStream) -> TokenStream {
80    let args = parse_macro_input!(attr as AggregateArgs);
81    let input = parse_macro_input!(item as ItemImpl);
82
83    let expanded = expand_aggregate(args, input);
84    TokenStream::from(expanded)
85}
86
87struct AggregateArgs {
88    domain: String,
89    state: Ident,
90}
91
92impl syn::parse::Parse for AggregateArgs {
93    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
94        let mut domain = None;
95        let mut state = None;
96
97        while !input.is_empty() {
98            let ident: Ident = input.parse()?;
99            input.parse::<Token![=]>()?;
100
101            match ident.to_string().as_str() {
102                "domain" => {
103                    let value: syn::LitStr = input.parse()?;
104                    domain = Some(value.value());
105                }
106                "state" => {
107                    let value: Ident = input.parse()?;
108                    state = Some(value);
109                }
110                _ => return Err(syn::Error::new(ident.span(), "unknown attribute")),
111            }
112
113            if input.peek(Token![,]) {
114                input.parse::<Token![,]>()?;
115            }
116        }
117
118        Ok(AggregateArgs {
119            domain: domain.ok_or_else(|| {
120                syn::Error::new(proc_macro2::Span::call_site(), "domain is required")
121            })?,
122            state: state.ok_or_else(|| {
123                syn::Error::new(proc_macro2::Span::call_site(), "state is required")
124            })?,
125        })
126    }
127}
128
129fn expand_aggregate(args: AggregateArgs, mut input: ItemImpl) -> TokenStream2 {
130    let domain = &args.domain;
131    let state_ty = &args.state;
132    let self_ty = &input.self_ty;
133
134    // Collect handler methods
135    let mut handlers = Vec::new();
136    let mut rejection_handlers = Vec::new();
137    let mut appliers = Vec::new();
138
139    for item in &input.items {
140        if let ImplItem::Fn(method) = item {
141            for attr in &method.attrs {
142                if attr.path().is_ident("handles") {
143                    if let Ok(command_type) = get_attr_ident(attr) {
144                        handlers.push((method.sig.ident.clone(), command_type));
145                    }
146                } else if attr.path().is_ident("rejected") {
147                    if let Ok((rej_domain, command)) = get_rejected_args(attr) {
148                        rejection_handlers.push((method.sig.ident.clone(), rej_domain, command));
149                    }
150                } else if attr.path().is_ident("applies") {
151                    if let Ok(event_type) = get_attr_ident(attr) {
152                        appliers.push((method.sig.ident.clone(), event_type));
153                    }
154                }
155            }
156        }
157    }
158
159    // Generate command type names
160    let command_types: Vec<_> = handlers
161        .iter()
162        .map(|(_, cmd_type)| {
163            let cmd_str = cmd_type.to_string();
164            quote! { #cmd_str.into() }
165        })
166        .collect();
167
168    // Generate handle dispatch arms
169    let handle_arms: Vec<_> = handlers
170        .iter()
171        .map(|(method, cmd_type)| {
172            let cmd_str = cmd_type.to_string();
173            quote! {
174                if payload.type_url.ends_with(#cmd_str) {
175                    let cmd = <#cmd_type as prost::Message>::decode(payload.value.as_slice())
176                        .map_err(|e| angzarr_client::CommandRejectedError::new(format!("Failed to decode {}: {}", #cmd_str, e)))?;
177                    return self.inner.#method(cmd_book, cmd, state, seq);
178                }
179            }
180        })
181        .collect();
182
183    // Generate rejection handler arms
184    let rejection_arms: Vec<_> = rejection_handlers
185        .iter()
186        .map(|(method, rej_domain, command)| {
187            quote! {
188                if target_domain == #rej_domain && target_command.ends_with(#command) {
189                    return self.inner.#method(notification, state);
190                }
191            }
192        })
193        .collect();
194
195    // Generate StateRouter .on() calls for each applier
196    let state_router_on_calls: Vec<_> = appliers
197        .iter()
198        .map(|(method, event_type)| {
199            quote! {
200                .on::<#event_type>(#self_ty::#method)
201            }
202        })
203        .collect();
204
205    // Remove our attributes from methods (they're not real Rust attributes)
206    for item in &mut input.items {
207        if let ImplItem::Fn(method) = item {
208            method.attrs.retain(|attr| {
209                !attr.path().is_ident("handles")
210                    && !attr.path().is_ident("rejected")
211                    && !attr.path().is_ident("applies")
212            });
213        }
214    }
215
216    // Generate the wrapper handler struct name
217    let handler_name = syn::Ident::new(
218        &format!("{}Handler", self_ty.to_token_stream()),
219        proc_macro2::Span::call_site(),
220    );
221
222    // Generate unique static name for the state router
223    let state_router_static = syn::Ident::new(
224        &format!("{}_STATE_ROUTER", self_ty.to_token_stream()).to_uppercase(),
225        proc_macro2::Span::call_site(),
226    );
227
228    quote! {
229        #input
230
231        /// Auto-generated state router with event appliers.
232        static #state_router_static: std::sync::LazyLock<angzarr_client::StateRouter<#state_ty>> =
233            std::sync::LazyLock::new(|| {
234                angzarr_client::StateRouter::new()
235                    #(#state_router_on_calls)*
236            });
237
238        /// Auto-generated handler wrapper implementing CommandHandlerDomainHandler.
239        pub struct #handler_name {
240            inner: #self_ty,
241        }
242
243        impl #handler_name {
244            pub fn new(inner: #self_ty) -> Self {
245                Self { inner }
246            }
247        }
248
249        impl angzarr_client::CommandHandlerDomainHandler for #handler_name {
250            type State = #state_ty;
251
252            fn command_types(&self) -> Vec<String> {
253                vec![#(#command_types),*]
254            }
255
256            fn state_router(&self) -> &angzarr_client::StateRouter<Self::State> {
257                &#state_router_static
258            }
259
260            fn handle(
261                &self,
262                cmd_book: &angzarr_client::proto::CommandBook,
263                payload: &prost_types::Any,
264                state: &Self::State,
265                seq: u32,
266            ) -> angzarr_client::CommandResult<angzarr_client::proto::EventBook> {
267                #(#handle_arms)*
268                Err(angzarr_client::CommandRejectedError::new(format!("Unknown command type: {}", payload.type_url)))
269            }
270
271            fn on_rejected(
272                &self,
273                notification: &angzarr_client::proto::Notification,
274                state: &Self::State,
275                target_domain: &str,
276                target_command: &str,
277            ) -> angzarr_client::CommandResult<angzarr_client::RejectionHandlerResponse> {
278                #(#rejection_arms)*
279                Ok(angzarr_client::RejectionHandlerResponse::default())
280            }
281        }
282
283        impl #self_ty {
284            /// Creates a CommandHandlerRouter from this aggregate's annotated methods.
285            pub fn into_router(self) -> angzarr_client::CommandHandlerRouter<#state_ty, #handler_name>
286            where
287                Self: Send + Sync + 'static,
288            {
289                angzarr_client::CommandHandlerRouter::new(#domain, #domain, #handler_name::new(self))
290            }
291        }
292    }
293}
294
295/// Marks a method as a command handler.
296///
297/// # Example
298/// ```rust,ignore
299/// #[handles(RegisterPlayer)]
300/// fn register(&self, cmd: RegisterPlayer, state: &PlayerState, seq: u32)
301///     -> CommandResult<EventBook> {
302///     // ...
303/// }
304/// ```
305#[proc_macro_attribute]
306pub fn handles(_attr: TokenStream, item: TokenStream) -> TokenStream {
307    // The actual work is done by the #[aggregate] macro
308    // This is just a marker attribute
309    item
310}
311
312/// Marks a method as a rejection handler.
313///
314/// # Attributes
315/// - `domain = "name"` - The domain of the rejected command
316/// - `command = "name"` - The type of the rejected command
317///
318/// # Example
319/// ```rust,ignore
320/// #[rejected(domain = "payment", command = "ProcessPayment")]
321/// fn handle_payment_rejected(&self, notification: &Notification, state: &PlayerState)
322///     -> CommandResult<BusinessResponse> {
323///     // ...
324/// }
325/// ```
326#[proc_macro_attribute]
327pub fn rejected(_attr: TokenStream, item: TokenStream) -> TokenStream {
328    // The actual work is done by the #[aggregate] or #[process_manager] macro
329    // This is just a marker attribute
330    item
331}
332
333/// Marks a method as an event applier for state reconstruction.
334///
335/// The method must be a static function with signature:
336/// `fn(state: &mut State, event: EventType)`
337///
338/// The #[aggregate] macro collects these and generates:
339/// - `apply_event(state, event_any)` - dispatches to the right applier
340/// - `rebuild(events)` - reconstructs state from event book
341///
342/// # Example
343/// ```rust,ignore
344/// #[applies(PlayerRegistered)]
345/// fn apply_registered(state: &mut PlayerState, event: PlayerRegistered) {
346///     state.player_id = format!("player_{}", event.email);
347///     state.display_name = event.display_name;
348///     state.exists = true;
349/// }
350///
351/// #[applies(FundsDeposited)]
352/// fn apply_deposited(state: &mut PlayerState, event: FundsDeposited) {
353///     if let Some(balance) = event.new_balance {
354///         state.bankroll = balance.amount;
355///     }
356/// }
357/// ```
358#[proc_macro_attribute]
359pub fn applies(_attr: TokenStream, item: TokenStream) -> TokenStream {
360    // The actual work is done by the #[aggregate] macro
361    // This is just a marker attribute
362    item
363}
364
365/// Marks an impl block as a saga with event handlers.
366///
367/// Sagas are pure translators: they receive source events and produce commands
368/// with deferred sequences. The framework handles sequence assignment on delivery.
369///
370/// # Attributes
371/// - `name = "saga-name"` - The saga's name (required)
372/// - `input = "domain"` - Input domain to listen to (required)
373///
374/// # Example
375/// ```rust,ignore
376/// #[saga(name = "saga-order-fulfillment", input = "order")]
377/// impl OrderFulfillmentSaga {
378///     #[handles(OrderCompleted)]
379///     fn handle_completed(&self, event: OrderCompleted, source: &EventBook)
380///         -> CommandResult<SagaHandlerResponse> {
381///         // Build commands with cover set (framework stamps angzarr_deferred)
382///         // ...
383///     }
384/// }
385/// ```
386#[proc_macro_attribute]
387pub fn saga(attr: TokenStream, item: TokenStream) -> TokenStream {
388    let args = parse_macro_input!(attr as SagaArgs);
389    let input = parse_macro_input!(item as ItemImpl);
390
391    let expanded = expand_saga(args, input);
392    TokenStream::from(expanded)
393}
394
395struct SagaArgs {
396    name: String,
397    input: String,
398}
399
400impl syn::parse::Parse for SagaArgs {
401    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
402        let mut name = None;
403        let mut input_domain = None;
404
405        while !input.is_empty() {
406            let ident: Ident = input.parse()?;
407            input.parse::<Token![=]>()?;
408            let value: syn::LitStr = input.parse()?;
409
410            match ident.to_string().as_str() {
411                "name" => name = Some(value.value()),
412                "input" => input_domain = Some(value.value()),
413                _ => return Err(syn::Error::new(ident.span(), "unknown attribute")),
414            }
415
416            if input.peek(Token![,]) {
417                input.parse::<Token![,]>()?;
418            }
419        }
420
421        Ok(SagaArgs {
422            name: name.ok_or_else(|| {
423                syn::Error::new(proc_macro2::Span::call_site(), "name is required")
424            })?,
425            input: input_domain.ok_or_else(|| {
426                syn::Error::new(proc_macro2::Span::call_site(), "input is required")
427            })?,
428        })
429    }
430}
431
432fn expand_saga(args: SagaArgs, mut input: ItemImpl) -> TokenStream2 {
433    let name = &args.name;
434    let input_domain = &args.input;
435    let self_ty = &input.self_ty;
436
437    // Collect handler methods
438    let mut event_handlers = Vec::new();
439
440    for item in &input.items {
441        if let ImplItem::Fn(method) = item {
442            for attr in &method.attrs {
443                if attr.path().is_ident("handles") {
444                    if let Ok(event_type) = get_attr_ident(attr) {
445                        event_handlers.push((method.sig.ident.clone(), event_type));
446                    }
447                }
448            }
449        }
450    }
451
452    // Generate event type names
453    let event_types: Vec<_> = event_handlers
454        .iter()
455        .map(|(_, event_type)| {
456            let event_str = event_type.to_string();
457            quote! { #event_str.into() }
458        })
459        .collect();
460
461    // Generate handle dispatch arms
462    let handle_arms: Vec<_> = event_handlers
463        .iter()
464        .map(|(method, event_type)| {
465            let event_str = event_type.to_string();
466            quote! {
467                if event.type_url.ends_with(#event_str) {
468                    let evt = <#event_type as prost::Message>::decode(event.value.as_slice())
469                        .map_err(|e| angzarr_client::CommandRejectedError::new(format!("Failed to decode {}: {}", #event_str, e)))?;
470                    return self.inner.#method(evt, source);
471                }
472            }
473        })
474        .collect();
475
476    // Remove our attributes from methods
477    for item in &mut input.items {
478        if let ImplItem::Fn(method) = item {
479            method.attrs.retain(|attr| !attr.path().is_ident("handles"));
480        }
481    }
482
483    // Generate the wrapper handler struct name
484    let handler_name = syn::Ident::new(
485        &format!("{}Handler", self_ty.to_token_stream()),
486        proc_macro2::Span::call_site(),
487    );
488
489    quote! {
490        #input
491
492        /// Auto-generated handler wrapper implementing SagaDomainHandler.
493        pub struct #handler_name {
494            inner: #self_ty,
495        }
496
497        impl #handler_name {
498            pub fn new(inner: #self_ty) -> Self {
499                Self { inner }
500            }
501        }
502
503        impl angzarr_client::SagaDomainHandler for #handler_name {
504            fn event_types(&self) -> Vec<String> {
505                vec![#(#event_types),*]
506            }
507
508            fn handle(
509                &self,
510                source: &angzarr_client::proto::EventBook,
511                event: &prost_types::Any,
512            ) -> angzarr_client::CommandResult<angzarr_client::SagaHandlerResponse> {
513                #(#handle_arms)*
514                Ok(angzarr_client::SagaHandlerResponse::default())
515            }
516        }
517
518        impl #self_ty {
519            /// Creates a SagaRouter from this saga's annotated methods.
520            pub fn into_router(self) -> angzarr_client::SagaRouter<#handler_name>
521            where
522                Self: Send + Sync + 'static,
523            {
524                angzarr_client::SagaRouter::new(#name, #input_domain, #handler_name::new(self))
525            }
526        }
527    }
528}
529
530/// Marks a method as a prepare handler for destination declaration.
531///
532/// # Example
533/// ```rust,ignore
534/// #[prepares(OrderCompleted)]
535/// fn prepare_order(&self, event: &OrderCompleted) -> Vec<Cover> {
536///     // ...
537/// }
538/// ```
539#[proc_macro_attribute]
540pub fn prepares(_attr: TokenStream, item: TokenStream) -> TokenStream {
541    item
542}
543
544/// Marks an impl block as a process manager with event handlers.
545///
546/// # Attributes
547/// - `name = "pm-name"` - The PM's name (required)
548/// - `domain = "pm-domain"` - The PM's own domain for state (required)
549/// - `state = StateType` - The PM's state type (required)
550/// - `inputs = ["domain1", "domain2"]` - Input domains to subscribe to (required)
551///
552/// # Example
553/// ```rust,ignore
554/// #[process_manager(name = "hand-flow", domain = "hand-flow", state = PMState, inputs = ["table", "hand"])]
555/// impl HandFlowPM {
556///     #[applies(PMStateUpdated)]
557///     fn apply_state(state: &mut PMState, event: PMStateUpdated) {
558///         // ...
559///     }
560///
561///     #[prepares(HandStarted)]
562///     fn prepare_hand(&self, trigger: &EventBook, state: &PMState, event: &HandStarted) -> Vec<Cover> {
563///         // ...
564///     }
565///
566///     #[handles(HandStarted)]
567///     fn handle_hand(&self, trigger: &EventBook, state: &PMState, event: HandStarted, destinations: &[EventBook])
568///         -> CommandResult<ProcessManagerResponse> {
569///         // ...
570///     }
571/// }
572/// ```
573#[proc_macro_attribute]
574pub fn process_manager(attr: TokenStream, item: TokenStream) -> TokenStream {
575    let args = parse_macro_input!(attr as ProcessManagerArgs);
576    let input = parse_macro_input!(item as ItemImpl);
577
578    let expanded = expand_process_manager(args, input);
579    TokenStream::from(expanded)
580}
581
582struct ProcessManagerArgs {
583    name: String,
584    domain: String,
585    state: Ident,
586    inputs: Vec<String>,
587}
588
589impl syn::parse::Parse for ProcessManagerArgs {
590    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
591        let mut name = None;
592        let mut domain = None;
593        let mut state = None;
594        let mut inputs = None;
595
596        while !input.is_empty() {
597            let ident: Ident = input.parse()?;
598            input.parse::<Token![=]>()?;
599
600            match ident.to_string().as_str() {
601                "name" => {
602                    let value: syn::LitStr = input.parse()?;
603                    name = Some(value.value());
604                }
605                "domain" => {
606                    let value: syn::LitStr = input.parse()?;
607                    domain = Some(value.value());
608                }
609                "state" => {
610                    let value: Ident = input.parse()?;
611                    state = Some(value);
612                }
613                "inputs" => {
614                    let content;
615                    syn::bracketed!(content in input);
616                    let mut domains = Vec::new();
617                    while !content.is_empty() {
618                        let lit: syn::LitStr = content.parse()?;
619                        domains.push(lit.value());
620                        if content.peek(Token![,]) {
621                            content.parse::<Token![,]>()?;
622                        }
623                    }
624                    inputs = Some(domains);
625                }
626                _ => return Err(syn::Error::new(ident.span(), "unknown attribute")),
627            }
628
629            if input.peek(Token![,]) {
630                input.parse::<Token![,]>()?;
631            }
632        }
633
634        Ok(ProcessManagerArgs {
635            name: name.ok_or_else(|| {
636                syn::Error::new(proc_macro2::Span::call_site(), "name is required")
637            })?,
638            domain: domain.ok_or_else(|| {
639                syn::Error::new(proc_macro2::Span::call_site(), "domain is required")
640            })?,
641            state: state.ok_or_else(|| {
642                syn::Error::new(proc_macro2::Span::call_site(), "state is required")
643            })?,
644            inputs: inputs.ok_or_else(|| {
645                syn::Error::new(proc_macro2::Span::call_site(), "inputs is required")
646            })?,
647        })
648    }
649}
650
651fn expand_process_manager(args: ProcessManagerArgs, mut input: ItemImpl) -> TokenStream2 {
652    let name = &args.name;
653    let pm_domain = &args.domain;
654    let state_ty = &args.state;
655    let inputs = &args.inputs;
656    let self_ty = &input.self_ty;
657
658    // Collect handler methods
659    let mut prepare_handlers = Vec::new();
660    let mut event_handlers = Vec::new();
661    let mut appliers = Vec::new();
662
663    for item in &input.items {
664        if let ImplItem::Fn(method) = item {
665            for attr in &method.attrs {
666                if attr.path().is_ident("prepares") {
667                    if let Ok(event_type) = get_attr_ident(attr) {
668                        prepare_handlers.push((method.sig.ident.clone(), event_type));
669                    }
670                } else if attr.path().is_ident("handles") {
671                    if let Ok(event_type) = get_attr_ident(attr) {
672                        event_handlers.push((method.sig.ident.clone(), event_type));
673                    }
674                } else if attr.path().is_ident("applies") {
675                    if let Ok(event_type) = get_attr_ident(attr) {
676                        appliers.push((method.sig.ident.clone(), event_type));
677                    }
678                }
679            }
680        }
681    }
682
683    // Generate event type names
684    let event_types: Vec<_> = event_handlers
685        .iter()
686        .map(|(_, event_type)| {
687            let event_str = event_type.to_string();
688            quote! { #event_str.into() }
689        })
690        .collect();
691
692    // Generate prepare dispatch arms
693    let prepare_arms: Vec<_> = prepare_handlers
694        .iter()
695        .map(|(method, event_type)| {
696            let event_str = event_type.to_string();
697            quote! {
698                if event.type_url.ends_with(#event_str) {
699                    if let Ok(evt) = <#event_type as prost::Message>::decode(event.value.as_slice()) {
700                        return self.inner.#method(trigger, state, &evt);
701                    }
702                }
703            }
704        })
705        .collect();
706
707    // Generate handle dispatch arms
708    let handle_arms: Vec<_> = event_handlers
709        .iter()
710        .map(|(method, event_type)| {
711            let event_str = event_type.to_string();
712            quote! {
713                if event.type_url.ends_with(#event_str) {
714                    let evt = <#event_type as prost::Message>::decode(event.value.as_slice())
715                        .map_err(|e| angzarr_client::CommandRejectedError::new(format!("Failed to decode {}: {}", #event_str, e)))?;
716                    return self.inner.#method(trigger, state, evt, destinations);
717                }
718            }
719        })
720        .collect();
721
722    // Generate apply_event dispatch arms
723    let apply_arms: Vec<_> = appliers
724        .iter()
725        .map(|(method, event_type)| {
726            let suffix = event_type.to_string();
727            quote! {
728                if event_any.type_url.ends_with(#suffix) {
729                    if let Ok(event) = <#event_type as prost::Message>::decode(event_any.value.as_slice()) {
730                        #self_ty::#method(state, event);
731                        return;
732                    }
733                }
734            }
735        })
736        .collect();
737
738    // Remove our attributes from methods
739    for item in &mut input.items {
740        if let ImplItem::Fn(method) = item {
741            method.attrs.retain(|attr| {
742                !attr.path().is_ident("prepares")
743                    && !attr.path().is_ident("handles")
744                    && !attr.path().is_ident("applies")
745            });
746        }
747    }
748
749    // Generate apply_event and rebuild functions if appliers exist
750    let apply_event_fn = if !appliers.is_empty() {
751        quote! {
752            /// Apply a single event to state. Auto-generated from #[applies] methods.
753            pub fn apply_event(state: &mut #state_ty, event_any: &prost_types::Any) {
754                #(#apply_arms)*
755                // Unknown event type - silently ignore (forward compatibility)
756            }
757
758            /// Rebuild state from event book. Auto-generated.
759            pub fn rebuild(events: &angzarr_client::proto::EventBook) -> #state_ty {
760                let mut state = #state_ty::default();
761                for page in &events.pages {
762                    if let Some(angzarr_client::proto::event_page::Payload::Event(event)) = &page.payload {
763                        Self::apply_event(&mut state, event);
764                    }
765                }
766                state
767            }
768        }
769    } else {
770        quote! {
771            /// Rebuild state from event book. Returns default state (no #[applies] methods).
772            pub fn rebuild(_events: &angzarr_client::proto::EventBook) -> #state_ty {
773                #state_ty::default()
774            }
775        }
776    };
777
778    // Generate the wrapper handler struct name
779    let handler_name = syn::Ident::new(
780        &format!("{}Handler", self_ty.to_token_stream()),
781        proc_macro2::Span::call_site(),
782    );
783
784    // Generate domain registrations
785    let domain_registrations: Vec<_> = inputs
786        .iter()
787        .map(|domain| {
788            quote! {
789                .domain(#domain, #handler_name { inner: inner.clone() })
790            }
791        })
792        .collect();
793
794    quote! {
795        #input
796
797        impl #self_ty {
798            #apply_event_fn
799        }
800
801        /// Auto-generated handler wrapper implementing ProcessManagerDomainHandler.
802        pub struct #handler_name {
803            inner: std::sync::Arc<#self_ty>,
804        }
805
806        impl angzarr_client::ProcessManagerDomainHandler<#state_ty> for #handler_name {
807            fn event_types(&self) -> Vec<String> {
808                vec![#(#event_types),*]
809            }
810
811            fn prepare(
812                &self,
813                trigger: &angzarr_client::proto::EventBook,
814                state: &#state_ty,
815                event: &prost_types::Any,
816            ) -> Vec<angzarr_client::proto::Cover> {
817                #(#prepare_arms)*
818                vec![]
819            }
820
821            fn handle(
822                &self,
823                trigger: &angzarr_client::proto::EventBook,
824                state: &#state_ty,
825                event: &prost_types::Any,
826                destinations: &[angzarr_client::proto::EventBook],
827            ) -> angzarr_client::CommandResult<angzarr_client::ProcessManagerResponse> {
828                #(#handle_arms)*
829                Ok(angzarr_client::ProcessManagerResponse::default())
830            }
831        }
832
833        impl #self_ty {
834            /// Creates a ProcessManagerRouter from this PM's annotated methods.
835            pub fn into_router(self) -> angzarr_client::ProcessManagerRouter<#state_ty>
836            where
837                Self: Send + Sync + 'static,
838            {
839                let inner = std::sync::Arc::new(self);
840                angzarr_client::ProcessManagerRouter::new(#name, #pm_domain, Self::rebuild)
841                    #(#domain_registrations)*
842            }
843        }
844    }
845}
846
847/// Marks a method as a projector event handler.
848///
849/// # Example
850/// ```rust,ignore
851/// #[projects(PlayerRegistered)]
852/// fn project_registered(&self, event: PlayerRegistered) -> Projection {
853///     // ...
854/// }
855/// ```
856#[proc_macro_attribute]
857pub fn projects(_attr: TokenStream, item: TokenStream) -> TokenStream {
858    item
859}
860
861/// Marks an impl block as a projector with event handlers.
862///
863/// # Attributes
864/// - `name = "projector-name"` - The projector's name (required)
865///
866/// # Example
867/// ```rust,ignore
868/// #[projector(name = "output")]
869/// impl OutputProjector {
870///     #[projects(PlayerRegistered)]
871///     fn project_registered(&self, event: PlayerRegistered) -> Projection {
872///         // ...
873///     }
874///
875///     #[projects(HandComplete)]
876///     fn project_hand_complete(&self, event: HandComplete) -> Projection {
877///         // ...
878///     }
879/// }
880/// ```
881#[proc_macro_attribute]
882pub fn projector(attr: TokenStream, item: TokenStream) -> TokenStream {
883    let args = parse_macro_input!(attr as ProjectorArgs);
884    let input = parse_macro_input!(item as ItemImpl);
885
886    let expanded = expand_projector(args, input);
887    TokenStream::from(expanded)
888}
889
890struct ProjectorArgs {
891    name: String,
892}
893
894impl syn::parse::Parse for ProjectorArgs {
895    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
896        let mut name = None;
897
898        while !input.is_empty() {
899            let ident: Ident = input.parse()?;
900            input.parse::<Token![=]>()?;
901
902            match ident.to_string().as_str() {
903                "name" => {
904                    let value: syn::LitStr = input.parse()?;
905                    name = Some(value.value());
906                }
907                _ => return Err(syn::Error::new(ident.span(), "unknown attribute")),
908            }
909
910            if input.peek(Token![,]) {
911                input.parse::<Token![,]>()?;
912            }
913        }
914
915        Ok(ProjectorArgs {
916            name: name.ok_or_else(|| {
917                syn::Error::new(proc_macro2::Span::call_site(), "name is required")
918            })?,
919        })
920    }
921}
922
923fn expand_projector(args: ProjectorArgs, mut input: ItemImpl) -> TokenStream2 {
924    let name = &args.name;
925    let self_ty = &input.self_ty;
926
927    // Collect handler methods
928    let mut event_handlers = Vec::new();
929
930    for item in &input.items {
931        if let ImplItem::Fn(method) = item {
932            for attr in &method.attrs {
933                if attr.path().is_ident("projects") {
934                    if let Ok(event_type) = get_attr_ident(attr) {
935                        event_handlers.push((method.sig.ident.clone(), event_type));
936                    }
937                }
938            }
939        }
940    }
941
942    // Generate event dispatch arms
943    let handler_arms: Vec<_> = event_handlers
944        .iter()
945        .map(|(method, event_type)| {
946            let suffix = event_type.to_string();
947            quote! {
948                if type_url.ends_with(#suffix) {
949                    if let Ok(event) = <#event_type as prost::Message>::decode(event_any.value.as_slice()) {
950                        return Some(self.#method(event));
951                    }
952                }
953            }
954        })
955        .collect();
956
957    // Generate the handle_event dispatch function
958    let dispatch_fn = if !event_handlers.is_empty() {
959        quote! {
960            /// Dispatch a single event to the appropriate handler.
961            fn handle_event(&self, event_any: &prost_types::Any) -> Option<angzarr_client::proto::Projection> {
962                let type_url = &event_any.type_url;
963                #(#handler_arms)*
964                None
965            }
966        }
967    } else {
968        quote! {
969            fn handle_event(&self, _event_any: &prost_types::Any) -> Option<angzarr_client::proto::Projection> {
970                None
971            }
972        }
973    };
974
975    // Remove our attributes from methods
976    for item in &mut input.items {
977        if let ImplItem::Fn(method) = item {
978            method.attrs.retain(|attr| !attr.path().is_ident("projects"));
979        }
980    }
981
982    quote! {
983        #input
984
985        impl #self_ty {
986            #dispatch_fn
987
988            /// Handle an EventBook by dispatching each event to handlers.
989            pub fn handle(&self, events: &angzarr_client::proto::EventBook) -> angzarr_client::proto::Projection {
990                let cover = events.cover.as_ref();
991                let mut last_seq = 0u32;
992
993                for page in &events.pages {
994                    if let Some(angzarr_client::proto::event_page::Payload::Event(event_any)) = &page.payload {
995                        if let Some(projection) = self.handle_event(event_any) {
996                            return projection;
997                        }
998                    }
999                    if let Some(header) = &page.header {
1000                        if let Some(angzarr_client::proto::page_header::SequenceType::Sequence(seq)) = &header.sequence_type {
1001                            last_seq = *seq;
1002                        }
1003                    }
1004                }
1005
1006                // Default projection if no handler matched
1007                angzarr_client::proto::Projection {
1008                    cover: cover.cloned(),
1009                    projector: #name.to_string(),
1010                    sequence: last_seq,
1011                    projection: None,
1012                }
1013            }
1014
1015            /// Creates a ProjectorHandler from this projector.
1016            pub fn into_handler(self) -> angzarr_client::ProjectorHandler
1017            where
1018                Self: Send + Sync + 'static,
1019            {
1020                let projector = std::sync::Arc::new(self);
1021                angzarr_client::ProjectorHandler::new(#name).with_handle_fn(move |events| {
1022                    Ok(projector.handle(events))
1023                })
1024            }
1025        }
1026    }
1027}
1028
1029// Helper functions
1030
1031fn get_attr_ident(attr: &Attribute) -> syn::Result<Ident> {
1032    let meta = attr.meta.clone();
1033    match meta {
1034        Meta::List(list) => {
1035            let ident: Ident = syn::parse2(list.tokens)?;
1036            Ok(ident)
1037        }
1038        _ => Err(syn::Error::new_spanned(attr, "expected #[attr(Type)]")),
1039    }
1040}
1041
1042fn get_rejected_args(attr: &Attribute) -> syn::Result<(String, String)> {
1043    let meta = attr.meta.clone();
1044    match meta {
1045        Meta::List(list) => {
1046            let args: RejectedArgs = syn::parse2(list.tokens)?;
1047            Ok((args.domain, args.command))
1048        }
1049        _ => Err(syn::Error::new_spanned(
1050            attr,
1051            "expected #[rejected(domain = \"...\", command = \"...\")]",
1052        )),
1053    }
1054}
1055
1056struct RejectedArgs {
1057    domain: String,
1058    command: String,
1059}
1060
1061impl syn::parse::Parse for RejectedArgs {
1062    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
1063        let mut domain = None;
1064        let mut command = None;
1065
1066        while !input.is_empty() {
1067            let ident: Ident = input.parse()?;
1068            input.parse::<Token![=]>()?;
1069            let value: syn::LitStr = input.parse()?;
1070
1071            match ident.to_string().as_str() {
1072                "domain" => domain = Some(value.value()),
1073                "command" => command = Some(value.value()),
1074                _ => return Err(syn::Error::new(ident.span(), "unknown attribute")),
1075            }
1076
1077            if input.peek(Token![,]) {
1078                input.parse::<Token![,]>()?;
1079            }
1080        }
1081
1082        Ok(RejectedArgs {
1083            domain: domain.ok_or_else(|| {
1084                syn::Error::new(proc_macro2::Span::call_site(), "domain is required")
1085            })?,
1086            command: command.ok_or_else(|| {
1087                syn::Error::new(proc_macro2::Span::call_site(), "command is required")
1088            })?,
1089        })
1090    }
1091}