actr-framework-protoc-codegen 0.3.0

Protoc plugin for generating actr-framework code from protobuf definitions
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
//! Modern code generator
//!
//! Generates code based on the actual actr-framework architecture:
//! - MessageDispatcher trait: zero-sized type static dispatcher
//! - Workload trait: business workload, associates Dispatcher type
//! - {Service}Handler trait: user-implemented business logic interface

use actr_protocol::ActrType;
use anyhow::{Result, anyhow};
use heck::ToSnakeCase;
use prost_types::MethodDescriptorProto;
use quote::{format_ident, quote};

use crate::payload_type_extractor::extract_payload_type_or_default;

/// Remote service information for dispatcher generation
#[derive(Debug, Clone)]
pub struct RemoteServiceInfo {
    pub package_name: String,
    pub service_name: String,
    pub methods: Vec<String>,
    pub actr_type: String,
}

/// Code generator role
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GeneratorRole {
    /// Generate export-side code for exports
    ServerSide,
    /// Generate dependency-side code for dependencies
    ClientSide,
}

/// Modern code generator
pub struct ModernGenerator {
    package_name: String,
    service_name: String,
    role: GeneratorRole,
}

impl ModernGenerator {
    pub fn new(package_name: &str, service_name: &str, role: GeneratorRole) -> Self {
        Self {
            package_name: package_name.to_string(),
            service_name: service_name.to_string(),
            role,
        }
    }

    /// Generate complete code
    pub fn generate(&self, methods: &[MethodDescriptorProto]) -> Result<String> {
        match self.role {
            GeneratorRole::ServerSide => self.generate_server_code(methods, &[]),
            GeneratorRole::ClientSide => self.generate_client_code(methods),
        }
    }

    /// Generate export-side code with remote forwarding
    pub fn generate_with_remotes(
        &self,
        methods: &[MethodDescriptorProto],
        remote_services: &[RemoteServiceInfo],
    ) -> Result<String> {
        match self.role {
            GeneratorRole::ServerSide => self.generate_server_code(methods, remote_services),
            GeneratorRole::ClientSide => self.generate_client_code(methods),
        }
    }

    /// Generate export-side code (exports)
    fn generate_server_code(
        &self,
        methods: &[MethodDescriptorProto],
        remote_services: &[RemoteServiceInfo],
    ) -> Result<String> {
        let sections = [
            // 1. Generate imports
            self.generate_imports(),
            // 2. Generate RpcRequest trait impls (type-safe Request -> Response association)
            self.generate_message_impls(methods)?,
            // 3. Generate Handler trait (user-implemented interface)
            self.generate_handler_trait(methods)?,
            // 4. Generate Dispatcher implementation (zero-sized type static dispatcher)
            self.generate_router_impl(methods, remote_services)?,
            // 5. Generate Workload blanket impl
            self.generate_workload_blanket_impl(methods)?,
            // 6. Generate usage docs
            self.generate_usage_docs(methods)?,
        ];

        Ok(sections.join("\n\n"))
    }

    /// Generate dependency-side code (dependencies)
    fn generate_client_code(&self, methods: &[MethodDescriptorProto]) -> Result<String> {
        let sections = [
            // 1. Generate imports
            self.generate_imports(),
            // 2. Generate RpcRequest trait impls (client also needs them for type-safe calls)
            self.generate_message_impls(methods)?,
            // 3. Generate Context extension methods
            self.generate_context_extensions(methods)?,
            // 4. Generate usage docs
            self.generate_client_usage_docs(methods)?,
        ];

        Ok(sections.join("\n\n"))
    }

    /// Generate import statements
    fn generate_imports(&self) -> String {
        // Generate protobuf message imports
        // Assume message types are in the sibling proto module (generated by prost)
        let proto_module = self.package_name.replace('.', "_");

        format!(
            r#"// Auto-generated code - DO NOT EDIT
// Generated by actr-cli's protoc-gen-actrframework plugin
#[allow(dead_code, unused_imports)]
use async_trait::async_trait;
use bytes::Bytes;
use prost::Message as ProstMessage;

use actr_framework::{{Context, Dest, MessageDispatcher, Workload}};
use actr_protocol::{{ActrId, ActorResult, RpcRequest, RpcEnvelope, PayloadType}};

// Import protobuf message types (generated by prost)
use super::{proto_module}::*;
"#
        )
    }

    /// Generate RpcRequest trait implementations
    ///
    /// Generates RpcRequest trait impls for each RPC method's Request type,
    /// associating it with the corresponding Response type. This enables
    /// type-safe API calls on the calling side:
    ///
    /// ```rust,ignore
    /// let response: EchoResponse = ctx.call(&target, request).await?;
    /// //              ^^^^^^^^^^^^ inferred from EchoRequest::Response
    /// ```
    fn generate_message_impls(&self, methods: &[MethodDescriptorProto]) -> Result<String> {
        let mut impls = Vec::new();

        for method in methods {
            let input_type = self.extract_message_type(method.input_type())?;
            let output_type = self.extract_message_type(method.output_type())?;

            // Generate route key
            let route_key = format!(
                "{}.{}.{}",
                self.package_name,
                self.service_name,
                method.name()
            );

            // Extract PayloadType
            let payload_type = extract_payload_type_or_default(method);

            // Generate PayloadType enum path (cannot use quote! as it adds quotes)
            let payload_type_code = payload_type.as_rust_variant();

            // Manually construct code string to avoid quote! adding quotes
            let impl_code = format!(
                r#"/// RpcRequest trait implementation - associates Request and Response types
///
/// This enables type-safe RPC calls with automatic response type inference:
/// ```rust,ignore
/// let response: {output_type} = ctx.call(&target, request).await?;
/// ```
impl RpcRequest for {input_type} {{
    type Response = {output_type};

    fn route_key() -> &'static str {{
        "{route_key}"
    }}

    fn payload_type() -> PayloadType {{
        {payload_type_code}
    }}
}}"#
            );

            impls.push(impl_code);
        }

        Ok(impls.join("\n\n"))
    }

    /// Generate Handler trait
    fn generate_handler_trait(&self, methods: &[MethodDescriptorProto]) -> Result<String> {
        let handler_trait_name = format!("{}Handler", self.service_name);
        let handler_trait_ident = format_ident!("{}", handler_trait_name);

        let mut method_sigs = Vec::new();
        for method in methods {
            let method_name = method.name().to_snake_case();
            let method_ident = format_ident!("{}", method_name);
            let input_type = self.extract_message_type(method.input_type())?;
            let output_type = self.extract_message_type(method.output_type())?;
            let input_ident = format_ident!("{}", input_type);
            let output_ident = format_ident!("{}", output_type);

            method_sigs.push(quote! {
                /// RPC method: #method_name
                async fn #method_ident<C: Context>(
                    &self,
                    req: #input_ident,
                    ctx: &C,
                ) -> ActorResult<#output_ident>;
            });
        }

        // Per Option U γ-unified §4.5 / Phase 6b the handler trait keeps
        // its minimal bound (`MaybeSendSync + 'static`) so user structs do
        // not need an extra `Sized` clause; the `Workload` association is
        // exposed through a separate `ServiceHandler` impl emitted for the
        // generated `{Service}Workload` wrapper (see
        // `generate_workload_blanket_impl`). Rust's orphan rules forbid a
        // blanket `impl ServiceHandler for T where T: {Service}Handler` on
        // the handler type itself — the wrapper shape avoids that by
        // putting the impl on a local type.
        let handler_trait_without_attr = quote! {
            /// Service handler trait - users must implement this trait
            ///
            /// # Example
            ///
            /// ```rust,ignore
            /// pub struct MyService { /* ... */ }
            ///
            /// #[async_trait]
            /// impl #handler_trait_ident for MyService {
            ///     async fn method_name(&self, req: Request, ctx: &Context) -> ActorResult<Response> {
            ///         // Business logic
            ///         Ok(Response::default())
            ///     }
            /// }
            /// ```
            pub trait #handler_trait_ident: actr_framework::MaybeSendSync + 'static {
                #(#method_sigs)*
            }
        };

        // Manually add #[async_trait] attribute to avoid quote! inserting spaces.
        //
        // `?Send` on wasm32 so the generated impl matches `Context`'s
        // per-target auto-trait contract (γ-unified §3.1). Native keeps the
        // default Send-future form so handler futures compose with tokio
        // multi-threaded executors.
        Ok(format!(
            "#[cfg_attr(not(target_arch = \"wasm32\"), async_trait)]\n\
             #[cfg_attr(target_arch = \"wasm32\", async_trait(?Send))]\n\
             {handler_trait_without_attr}"
        ))
    }

    /// Generate Dispatcher and Workload wrapper types
    fn generate_router_impl(
        &self,
        methods: &[MethodDescriptorProto],
        remote_services: &[RemoteServiceInfo],
    ) -> Result<String> {
        let router_name = format!("{}Dispatcher", self.service_name);
        let router_ident = format_ident!("{}", router_name);
        let workload_name = format!("{}Workload", self.service_name);
        let workload_ident = format_ident!("{}", workload_name);
        let handler_trait = format!("{}Handler", self.service_name);
        let handler_trait_ident = format_ident!("{}", handler_trait);

        // Generate match arms for local service
        let mut match_arms = Vec::new();
        for method in methods {
            let route_key = format!(
                "{}.{}.{}",
                self.package_name,
                self.service_name,
                method.name()
            );
            let method_name = method.name().to_snake_case();
            let method_ident = format_ident!("{}", method_name);
            let input_type = self.extract_message_type(method.input_type())?;
            let input_ident = format_ident!("{}", input_type);

            match_arms.push(quote! {
                #route_key => {
                    // Extract payload from envelope
                    let payload = envelope.payload.as_ref()
                        .ok_or_else(|| actr_protocol::ActrError::DecodeFailure(
                            "Missing payload in RpcEnvelope".to_string()
                        ))?;

                    // Deserialize request
                    let req = #input_ident::decode(&**payload)
                        .map_err(|e| actr_protocol::ActrError::DecodeFailure(
                            format!("Failed to decode {}: {}", stringify!(#input_ident), e)
                        ))?;

                    // Call business logic
                    let resp = workload.0.#method_ident(req, ctx).await?;

                    // Serialize response
                    Ok(resp.encode_to_vec().into())
                }
            });
        }

        // Generate forwarding arms for remote services
        // Group remote services by actr_type
        use std::collections::HashMap;
        let mut services_by_actr_type: HashMap<String, Vec<&RemoteServiceInfo>> = HashMap::new();
        for remote_service in remote_services {
            services_by_actr_type
                .entry(remote_service.actr_type.clone())
                .or_default()
                .push(remote_service);
        }

        // Generate forwarding cases for each actr_type
        for (actr_type_str, services) in services_by_actr_type {
            let parsed = ActrType::from_string_repr(&actr_type_str).map_err(|e| {
                anyhow!(
                    "Invalid remote actr_type '{}': expected <manufacturer>:<name>[:<version>] ({})",
                    actr_type_str,
                    e
                )
            })?;
            let manufacturer = parsed.manufacturer;
            let name = parsed.name;

            // Collect all route keys for this actr_type
            let mut route_keys = Vec::new();
            for service in &services {
                for method in &service.methods {
                    let route_key = format!(
                        "{}.{}.{}",
                        service.package_name, service.service_name, method
                    );
                    route_keys.push(route_key);
                }
            }

            // Generate match arm for all route keys of this actr_type
            match_arms.push(quote! {
                #(#route_keys)|* => {
                    let target_type = actr_protocol::ActrType {
                        manufacturer: #manufacturer.to_string(),
                        name: #name.to_string(),
                        version: "1.0.0".to_string(),
                    };
                    let target_id = ctx.discover_route_candidate(&target_type).await?;
                    ctx.call_raw(
                        &target_id,
                        envelope.route_key.as_str(),
                        envelope.payload.clone().unwrap_or_default(),
                    ).await
                }
            });
        }

        // Generate each section separately to ensure correct attribute output
        let workload_struct = quote! {
            /// Workload wrapper type
            ///
            /// Wraps the user's Handler implementation to satisfy orphan rules
            pub struct #workload_ident<T: #handler_trait_ident>(pub T);

            impl<T: #handler_trait_ident> #workload_ident<T> {
                /// Create a new Workload instance
                pub fn new(handler: T) -> Self {
                    Self(handler)
                }
            }
        };

        let router_struct = quote! {
            /// Message dispatcher - zero-sized type (ZST)
            ///
            /// This router is auto-generated by the code generator, statically routing
            /// route_key to the corresponding handler method.
            ///
            /// # Performance characteristics
            ///
            /// - Zero memory overhead (PhantomData)
            /// - Static match dispatch, ~5-10ns
            /// - Full compiler inlining
            pub struct #router_ident<T: #handler_trait_ident>(std::marker::PhantomData<T>);
        };

        let router_impl_without_attr = quote! {
            impl<T: #handler_trait_ident> MessageDispatcher for #router_ident<T> {
                type Workload = #workload_ident<T>;

                async fn dispatch<C: Context>(
                    workload: &Self::Workload,
                    envelope: RpcEnvelope,
                    ctx: &C,
                ) -> ActorResult<Bytes> {
                    match envelope.route_key.as_str() {
                        #(#match_arms,)*
                        _ => Err(actr_protocol::ActrError::UnknownRoute(
                            envelope.route_key.to_string()
                        ))
                    }
                }
            }
        };

        // Manually add #[async_trait] attribute to avoid quote! inserting spaces.
        // Same `?Send` on wasm32 rationale as the Handler trait above — the
        // `MessageDispatcher` trait is declared `async_trait(?Send)` on
        // wasm32 in `core/framework/src/dispatcher.rs`, so the impl has to
        // match or the async future's auto-trait bound will disagree.
        let router_impl = format!(
            "#[cfg_attr(not(target_arch = \"wasm32\"), async_trait)]\n\
             #[cfg_attr(target_arch = \"wasm32\", async_trait(?Send))]\n\
             {router_impl_without_attr}"
        );

        Ok(format!("{workload_struct}\n{router_struct}\n{router_impl}"))
    }

    /// Generate Workload implementation
    fn generate_workload_blanket_impl(&self, _methods: &[MethodDescriptorProto]) -> Result<String> {
        let router_name = format!("{}Dispatcher", self.service_name);
        let router_ident = format_ident!("{}", router_name);
        let workload_name = format!("{}Workload", self.service_name);
        let workload_ident = format_ident!("{}", workload_name);
        let handler_trait = format!("{}Handler", self.service_name);
        let handler_trait_ident = format_ident!("{}", handler_trait);

        // `ServiceHandler` impl goes on the generated `{Service}Workload`
        // wrapper (a local type) rather than on the user's handler type
        // — the latter would need a blanket `impl<T> ForeignTrait for T
        // where T: LocalTrait` that Rust's orphan rule rejects. Putting
        // the impl on the local wrapper keeps coherence happy and still
        // lets downstream code recover the concrete `Workload` type by
        // projecting `<{Service}Workload<T> as ServiceHandler>::Workload`.
        //
        // `type Workload = Self` because the wrapper *is* the workload
        // (it already carries `impl Workload for {Service}Workload<T>`);
        // the association is purely reflexive.
        Ok(quote! {
            /// Workload trait implementation
            ///
            /// Implements Workload for the wrapper type so it can be recognized
            /// and dispatched by ActorSystem
            impl<T: #handler_trait_ident> Workload for #workload_ident<T> {
                type Dispatcher = #router_ident<T>;
            }

            /// `ServiceHandler` witness: expose the workload type via
            /// associated-type projection.
            ///
            /// Emitted on the generated wrapper (a local type) rather than
            /// on the user's handler to stay within Rust's orphan rules.
            /// `entry!` and other meta-macros consult
            /// `<{Service}Workload<T> as actr_framework::ServiceHandler>::Workload`
            /// to recover the concrete workload type.
            impl<T: #handler_trait_ident> actr_framework::ServiceHandler for #workload_ident<T> {
                type Workload = Self;
            }
        }
        .to_string())
    }

    /// Generate Context extension methods (client)
    fn generate_context_extensions(&self, methods: &[MethodDescriptorProto]) -> Result<String> {
        let client_struct_name = format!("{}Client", self.service_name);
        let client_ident = format_ident!("{}", client_struct_name);

        let mut client_methods = Vec::new();
        for method in methods {
            let method_name = method.name().to_snake_case();
            let method_ident = format_ident!("{}", method_name);
            let input_type = self.extract_message_type(method.input_type())?;
            let output_type = self.extract_message_type(method.output_type())?;
            let input_ident = format_ident!("{}", input_type);
            let output_ident = format_ident!("{}", output_type);

            client_methods.push(quote! {
                /// Call remote method: #method_name
                pub async fn #method_ident(
                    &self,
                    target: ActrId,
                    req: #input_ident,
                ) -> ActorResult<#output_ident> {
                    self.ctx.call(&Dest::from(target), req).await
                }
            });
        }

        // Generate Context extension
        let extension_method_name = self.service_name.to_snake_case();
        let extension_method_ident = format_ident!("{}", extension_method_name);

        Ok(quote! {
            /// Client interface
            ///
            /// Provides type-safe remote call methods
            pub struct #client_ident<'a, C: Context> {
                ctx: &'a C,
            }

            impl<'a, C: Context> #client_ident<'a, C> {
                #(#client_methods)*
            }

            /// Context extension trait
            ///
            /// Adds convenient caller methods to Context
            pub trait ContextExt {
                fn #extension_method_ident(&self) -> #client_ident<'_, Self> where Self: Sized + Context;
            }

            impl<T: Context> ContextExt for T {
                fn #extension_method_ident(&self) -> #client_ident<'_, Self> {
                    #client_ident { ctx: self }
                }
            }
        }
        .to_string())
    }

    /// Generate export-side usage docs
    fn generate_usage_docs(&self, methods: &[MethodDescriptorProto]) -> Result<String> {
        let handler_trait = format!("{}Handler", self.service_name);
        let first_method = methods.first();

        let example_method = if let Some(method) = first_method {
            let method_name = method.name().to_snake_case();
            let input_type = self.extract_message_type(method.input_type())?;
            let output_type = self.extract_message_type(method.output_type())?;
            format!(
                r#"
    async fn {method_name}(&self, req: {input_type}, ctx: &Context) -> ActorResult<{output_type}> {{
        // Implement business logic
        Ok({output_type}::default())
    }}"#
            )
        } else {
            "    // Implement methods...".to_string()
        };

        Ok(format!(
            r#"/*
## Usage Example

### 1. Implement Business Logic

```rust
use actr_framework::Context;
use actr_protocol::ActorResult;
use async_trait::async_trait;

pub struct MyService {{
    // Business state
}}

#[async_trait]
impl {handler_trait} for MyService {{
{example_method}
}}
```

### 2. Register the Entry Point

```rust
actr_framework::entry!({}Workload<MyService>);
```

## Architecture

- **{handler_trait}**: user-implemented business logic interface
- **{}Dispatcher**: zero-sized type static dispatcher (auto-generated)
- **{}Workload<T>**: generated wrapper that satisfies orphan rules

Users only need to implement {handler_trait}; the framework auto-provides routing and workload capabilities.
*/
"#,
            self.service_name, self.service_name, self.service_name
        ))
    }

    /// Generate dependency-side usage docs
    fn generate_client_usage_docs(&self, methods: &[MethodDescriptorProto]) -> Result<String> {
        let service_name_snake = self.service_name.to_snake_case();
        let method_name_snake = methods
            .first()
            .map(|m| m.name().to_snake_case())
            .unwrap_or("unknown_method".to_string());

        Ok(format!(
            r#"/*
## Dependency Usage Example

```rust
use actr_framework::Context;
use actr_protocol::{{ActorResult, ActrId}};

async fn call_remote_service(ctx: &impl Context, target: ActrId) -> ActorResult<()> {{
    use super::ContextExt;

    // Type-safe remote call
    let response = ctx.{service_name_snake}()
        .{method_name_snake}(target, request)
        .await?;

    Ok(())
}}
```

## Compile-time Routing

All remote calls determine the target service and method at compile time — no runtime lookup needed.
*/
"#
        ))
    }

    /// Extract message type name
    fn extract_message_type(&self, type_name: &str) -> Result<String> {
        let cleaned = type_name.trim_start_matches('.');
        if let Some(last_part) = cleaned.split('.').next_back() {
            Ok(last_part.to_string())
        } else {
            Ok(cleaned.to_string())
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use prost_types::MethodDescriptorProto;

    #[test]
    fn test_extract_message_type() {
        let generator = ModernGenerator::new("test.v1", "TestService", GeneratorRole::ServerSide);

        assert_eq!(
            generator
                .extract_message_type(".test.v1.EchoRequest")
                .unwrap(),
            "EchoRequest"
        );
        assert_eq!(
            generator
                .extract_message_type("test.v1.EchoResponse")
                .unwrap(),
            "EchoResponse"
        );
        assert_eq!(
            generator.extract_message_type("SimpleMessage").unwrap(),
            "SimpleMessage"
        );
    }

    #[test]
    fn test_generate_message_impls_includes_payload_type() {
        let generator = ModernGenerator::new("test.v1", "TestService", GeneratorRole::ServerSide);

        let methods = vec![MethodDescriptorProto {
            name: Some("Echo".to_string()),
            input_type: Some(".test.v1.EchoRequest".to_string()),
            output_type: Some(".test.v1.EchoResponse".to_string()),
            options: None,
            ..Default::default()
        }];

        let result = generator.generate_message_impls(&methods).unwrap();

        // Debug: print generated code
        eprintln!("Generated code:\n{result}");

        // Verify generated code contains payload_type() method
        assert!(
            result.contains("fn payload_type"),
            "Should contain 'fn payload_type'"
        );
        assert!(
            result.contains("PayloadType"),
            "Should contain 'PayloadType'"
        );
        // Verify default is RpcReliable
        assert!(
            result.contains("RpcReliable"),
            "Should contain 'RpcReliable'"
        );
    }

    #[test]
    fn test_generate_imports_includes_payload_type() {
        let generator = ModernGenerator::new("test.v1", "TestService", GeneratorRole::ServerSide);
        let imports = generator.generate_imports();

        // Verify PayloadType is imported
        assert!(imports.contains("PayloadType"));
        assert!(imports.contains(
            "use actr_protocol::{ActrId, ActorResult, RpcRequest, RpcEnvelope, PayloadType}"
        ));
        assert!(
            imports.contains("use actr_framework::{Context, Dest, MessageDispatcher, Workload}")
        );
    }

    #[test]
    fn test_generate_client_code() {
        let generator = ModernGenerator::new("test.v1", "TestService", GeneratorRole::ClientSide);

        let methods = vec![MethodDescriptorProto {
            name: Some("Echo".to_string()),
            input_type: Some(".test.v1.EchoRequest".to_string()),
            output_type: Some(".test.v1.EchoResponse".to_string()),
            options: None,
            ..Default::default()
        }];

        let result = generator.generate(&methods);
        assert!(result.is_ok());

        let code = result.unwrap();
        // Client code should also contain RpcRequest impl
        assert!(code.contains("impl RpcRequest for EchoRequest"));
        assert!(code.contains("fn payload_type() -> PayloadType"));
        assert!(code.contains("use actr_framework::{Context, Dest, MessageDispatcher, Workload}"));
        assert!(code.contains(
            "use actr_protocol::{ActrId, ActorResult, RpcRequest, RpcEnvelope, PayloadType}"
        ));
    }

    #[test]
    fn test_generate_server_code() {
        let generator = ModernGenerator::new("test.v1", "TestService", GeneratorRole::ServerSide);

        let methods = vec![MethodDescriptorProto {
            name: Some("Echo".to_string()),
            input_type: Some(".test.v1.EchoRequest".to_string()),
            output_type: Some(".test.v1.EchoResponse".to_string()),
            options: None,
            ..Default::default()
        }];

        let result = generator.generate(&methods);
        assert!(result.is_ok());

        let code = result.unwrap();
        // Verify Handler trait was generated
        assert!(code.contains("pub trait TestServiceHandler"));
        // Verify Dispatcher was generated
        assert!(code.contains("pub struct TestServiceDispatcher"));
        // Verify payload_type was generated
        assert!(code.contains("fn payload_type() -> PayloadType"));
    }

    #[test]
    fn test_generate_server_code_with_no_local_methods() {
        let generator = ModernGenerator::new("test.v1", "BridgeService", GeneratorRole::ServerSide);

        let code = generator.generate(&[]).unwrap();

        assert!(code.contains("pub trait BridgeServiceHandler"));
        assert!(code.contains("pub struct BridgeServiceWorkload"));
        assert!(code.contains("pub struct BridgeServiceDispatcher"));
        assert!(code.contains("UnknownRoute"));
    }

    #[test]
    fn test_generate_server_code_with_remote_forwarding_and_no_local_methods() {
        let generator =
            ModernGenerator::new("demo.app", "DemoClientApp", GeneratorRole::ServerSide);
        let remote_services = vec![RemoteServiceInfo {
            package_name: "echo".to_string(),
            service_name: "EchoService".to_string(),
            methods: vec!["Echo".to_string()],
            actr_type: "acme:EchoService:1.0.0".to_string(),
        }];

        let code = generator
            .generate_with_remotes(&[], &remote_services)
            .unwrap();

        assert!(code.contains("pub trait DemoClientAppHandler"));
        assert!(code.contains("pub struct DemoClientAppWorkload"));
        assert!(code.contains("\"echo.EchoService.Echo\""));
        assert!(code.contains("manufacturer"));
        assert!(code.contains("\"acme\""));
        assert!(code.contains("name"));
        assert!(code.contains("\"EchoService\""));
        assert!(code.contains("discover_route_candidate"));
        assert!(code.contains("call_raw"));
    }
}