Skip to main content

dubbo_rs_codegen/
lib.rs

1pub use serde;
2
3use std::collections::HashMap;
4use std::fmt::Write;
5use std::path::{Path, PathBuf};
6
7// ---------------------------------------------------------------------------
8// Public configuration types
9// ---------------------------------------------------------------------------
10
11/// Client code generation mode.
12#[derive(Clone, Debug, Default, PartialEq, Eq)]
13pub enum ClientMode {
14    /// Generate channel-based client wrapping tonic generated client.
15    Channel,
16    /// Generate invoker-based client using `Box<dyn Invoker>`.
17    Invoker,
18    /// Generate both channel and invoker clients.
19    #[default]
20    Both,
21}
22
23#[derive(Clone, Debug)]
24pub struct GeneratorConfig {
25    pub proto_paths: Vec<PathBuf>,
26    pub output_dir: Option<PathBuf>,
27    pub enable_client: bool,
28    pub enable_server: bool,
29    pub client_mode: ClientMode,
30}
31
32impl Default for GeneratorConfig {
33    fn default() -> Self {
34        Self {
35            proto_paths: vec![PathBuf::from("proto")],
36            output_dir: Some(PathBuf::from("src/gen")),
37            enable_client: true,
38            enable_server: true,
39            client_mode: ClientMode::default(),
40        }
41    }
42}
43
44#[derive(Debug)]
45pub struct GeneratorConfigBuilder {
46    proto_paths: Vec<PathBuf>,
47    output_dir: Option<PathBuf>,
48    enable_client: bool,
49    enable_server: bool,
50    client_mode: ClientMode,
51}
52
53impl Default for GeneratorConfigBuilder {
54    fn default() -> Self {
55        Self {
56            proto_paths: Vec::new(),
57            output_dir: None,
58            enable_client: true,
59            enable_server: true,
60            client_mode: ClientMode::default(),
61        }
62    }
63}
64
65impl GeneratorConfigBuilder {
66    #[must_use]
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    #[must_use]
72    pub fn proto_path(mut self, path: impl Into<PathBuf>) -> Self {
73        self.proto_paths.push(path.into());
74        self
75    }
76
77    #[must_use]
78    pub fn output_dir(mut self, dir: impl Into<PathBuf>) -> Self {
79        self.output_dir = Some(dir.into());
80        self
81    }
82
83    #[must_use]
84    pub fn enable_client(mut self, enable: bool) -> Self {
85        self.enable_client = enable;
86        self
87    }
88
89    #[must_use]
90    pub fn enable_server(mut self, enable: bool) -> Self {
91        self.enable_server = enable;
92        self
93    }
94
95    #[must_use]
96    pub fn client_mode(mut self, mode: ClientMode) -> Self {
97        self.client_mode = mode;
98        self
99    }
100
101    /// # Errors
102    ///
103    /// Returns an error if `proto_paths` is empty.
104    pub fn build(self) -> anyhow::Result<GeneratorConfig> {
105        if self.proto_paths.is_empty() {
106            anyhow::bail!("proto_paths must not be empty");
107        }
108        Ok(GeneratorConfig {
109            proto_paths: self.proto_paths,
110            output_dir: self.output_dir,
111            enable_client: self.enable_client,
112            enable_server: self.enable_server,
113            client_mode: self.client_mode,
114        })
115    }
116}
117
118#[derive(Debug, Default)]
119pub struct GeneratedCode {
120    pub files: HashMap<String, String>,
121}
122
123impl GeneratedCode {
124    #[must_use]
125    pub fn new() -> Self {
126        Self::default()
127    }
128
129    pub fn insert(&mut self, name: String, content: String) {
130        self.files.insert(name, content);
131    }
132
133    /// # Errors
134    ///
135    /// Returns an error if directory creation fails or if a file cannot be written.
136    pub fn write_to_dir(&self, dir: &Path) -> anyhow::Result<()> {
137        std::fs::create_dir_all(dir)?;
138        for (name, content) in &self.files {
139            std::fs::write(dir.join(name), content)?;
140        }
141        Ok(())
142    }
143
144    #[must_use]
145    pub fn is_empty(&self) -> bool {
146        self.files.is_empty()
147    }
148}
149
150// ---------------------------------------------------------------------------
151// Parsed proto definitions
152// ---------------------------------------------------------------------------
153
154struct ProtoService {
155    name: String,
156    methods: Vec<ProtoMethod>,
157}
158
159struct ProtoMethod {
160    name: String,
161    input_type: String,
162    output_type: String,
163    client_streaming: bool,
164    server_streaming: bool,
165}
166
167struct ProtoInfo {
168    package: String,
169    services: Vec<ProtoService>,
170}
171
172// ---------------------------------------------------------------------------
173// String helpers
174// ---------------------------------------------------------------------------
175
176#[cfg(test)]
177fn to_pascal_case(s: &str) -> String {
178    s.split(['_', '-'])
179        .filter(|word| !word.is_empty())
180        .map(|word| {
181            let mut chars = word.chars();
182            match chars.next() {
183                None => String::new(),
184                Some(c) => {
185                    let upper: String = c.to_uppercase().collect();
186                    upper + chars.as_str()
187                }
188            }
189        })
190        .collect()
191}
192
193fn to_snake_case(s: &str) -> String {
194    let mut result = String::with_capacity(s.len() + s.len() / 2);
195    let chars: Vec<char> = s.chars().collect();
196    for (i, c) in chars.iter().enumerate() {
197        if c.is_uppercase() {
198            let prev_lower = i > 0 && chars[i - 1].is_lowercase();
199            let next_lower = i + 1 < chars.len() && chars[i + 1].is_lowercase();
200            if i > 0 && (prev_lower || next_lower) {
201                result.push('_');
202            }
203            result.push(c.to_ascii_lowercase());
204        } else {
205            result.push(*c);
206        }
207    }
208    result
209}
210
211// ---------------------------------------------------------------------------
212// Proto text parser
213// ---------------------------------------------------------------------------
214
215fn strip_comments(input: &str) -> String {
216    let mut result = String::with_capacity(input.len());
217    let chars: Vec<char> = input.chars().collect();
218    let mut i = 0;
219    while i < chars.len() {
220        if i + 1 < chars.len() && chars[i] == '/' && chars[i + 1] == '/' {
221            while i < chars.len() && chars[i] != '\n' {
222                i += 1;
223            }
224        } else if i + 1 < chars.len() && chars[i] == '/' && chars[i + 1] == '*' {
225            i += 2;
226            while i + 1 < chars.len() && !(chars[i] == '*' && chars[i + 1] == '/') {
227                i += 1;
228            }
229            i += 2;
230        } else {
231            result.push(chars[i]);
232            i += 1;
233        }
234    }
235    result
236}
237
238fn parse_proto_content(content: &str) -> ProtoInfo {
239    let cleaned = strip_comments(content);
240
241    let package = regex_capture_package(&cleaned).unwrap_or_default();
242
243    let services = parse_services(&cleaned);
244
245    ProtoInfo { package, services }
246}
247
248fn regex_capture_package(input: &str) -> Option<String> {
249    let pat = "package ";
250    let start = input.find(pat)?;
251    let rest = &input[start + pat.len()..];
252    let end = rest.find(';')?;
253    Some(rest[..end].trim().to_string())
254}
255
256fn parse_services(cleaned: &str) -> Vec<ProtoService> {
257    let mut services = Vec::new();
258
259    for svc_match in find_all_occurrences(cleaned, "service ") {
260        if let Some(svc) = parse_one_service(cleaned, svc_match) {
261            services.push(svc);
262        }
263    }
264
265    services
266}
267
268fn find_all_occurrences(haystack: &str, needle: &str) -> Vec<usize> {
269    haystack.match_indices(needle).map(|(i, _)| i).collect()
270}
271
272fn parse_one_service(cleaned: &str, start: usize) -> Option<ProtoService> {
273    let remaining = &cleaned[start..];
274
275    let first_brace = remaining.find('{')?;
276    let header = &remaining[..first_brace];
277
278    let name = header.trim().strip_prefix("service ")?.trim().to_string();
279
280    let body_start = start + first_brace + 1;
281    let brace_end = find_matching_brace(cleaned, body_start)?;
282    let body = &cleaned[body_start..brace_end];
283
284    let methods = parse_rpc_methods(body);
285
286    Some(ProtoService { name, methods })
287}
288
289fn find_matching_brace(s: &str, start: usize) -> Option<usize> {
290    let mut depth = 1i32;
291    let chars: Vec<char> = s.chars().collect();
292    let mut pos = start;
293    while pos < chars.len() && depth > 0 {
294        match chars.get(pos)? {
295            '{' => depth += 1,
296            '}' => depth -= 1,
297            _ => {}
298        }
299        pos += 1;
300    }
301    if depth == 0 {
302        Some(pos - 1)
303    } else {
304        None
305    }
306}
307
308fn parse_rpc_methods(body: &str) -> Vec<ProtoMethod> {
309    let mut methods = Vec::new();
310
311    for rpc_match in find_all_occurrences(body, "rpc ") {
312        if let Some(method) = parse_one_rpc(body, rpc_match) {
313            methods.push(method);
314        }
315    }
316
317    methods
318}
319
320fn parse_one_rpc(body: &str, start: usize) -> Option<ProtoMethod> {
321    let remaining = &body[start..];
322
323    let semi_or_brace = remaining.find([';', '{']).unwrap_or(remaining.len());
324    let rpc_text = &remaining[..semi_or_brace];
325
326    // Format: rpc MethodName (stream? InputType) returns (stream? OutputType)
327    let trimmed = rpc_text.trim().strip_prefix("rpc ")?;
328    let paren_start = trimmed.find('(')?;
329    let name = trimmed[..paren_start].trim().to_string();
330
331    let rest = &trimmed[paren_start..];
332
333    // Parse input: (stream? InputType)
334    let inner = extract_between_parens(rest)?;
335    let (client_streaming, input_type) = if inner.trim().starts_with("stream ") {
336        (
337            true,
338            inner.trim().strip_prefix("stream ")?.trim().to_string(),
339        )
340    } else {
341        (false, inner.trim().to_string())
342    };
343
344    // Find "returns" keyword
345    let returns_pos = rest.find("returns")?;
346    let returns_part = &rest[returns_pos + "returns".len()..];
347
348    let output_inner = extract_between_parens(returns_part)?;
349    let (server_streaming, output_type) = if output_inner.trim().starts_with("stream ") {
350        (
351            true,
352            output_inner
353                .trim()
354                .strip_prefix("stream ")?
355                .trim()
356                .to_string(),
357        )
358    } else {
359        (false, output_inner.trim().to_string())
360    };
361
362    Some(ProtoMethod {
363        name,
364        input_type,
365        output_type,
366        client_streaming,
367        server_streaming,
368    })
369}
370
371fn extract_between_parens(s: &str) -> Option<&str> {
372    let start = s.find('(')?;
373    let end = s[start + 1..].find(')')?;
374    Some(&s[start + 1..start + 1 + end])
375}
376
377// ---------------------------------------------------------------------------
378// Code generation
379// ---------------------------------------------------------------------------
380
381pub struct CodeGenerator {
382    config: GeneratorConfig,
383}
384
385impl CodeGenerator {
386    #[must_use]
387    pub fn new(config: GeneratorConfig) -> Self {
388        Self { config }
389    }
390
391    #[must_use]
392    pub fn builder() -> GeneratorConfigBuilder {
393        GeneratorConfigBuilder::new()
394    }
395
396    /// # Errors
397    ///
398    /// Returns an error if no proto files are found, if protobuf compilation fails,
399    /// or if configured paths are invalid.
400    pub fn generate(&self) -> anyhow::Result<GeneratedCode> {
401        let mut generated = GeneratedCode::new();
402
403        let (proto_files, include_dirs) = self.collect_proto_files()?;
404
405        if proto_files.is_empty() {
406            anyhow::bail!("no .proto files found in configured paths");
407        }
408
409        let proto_infos = Self::parse_proto_files(&proto_files)?;
410
411        self.compile_with_tonic(&proto_files, &include_dirs)?;
412
413        for proto_info in &proto_infos {
414            if proto_info.services.is_empty() {
415                continue;
416            }
417            let module_name = proto_info.package.replace('.', "_");
418            let file_name = format!("{module_name}_dubbo.rs");
419            let code = self.render_dubbo_integration(proto_info)?;
420            generated.insert(file_name, code);
421        }
422
423        let build_rs = Self::render_build_rs_template(&proto_files, &include_dirs);
424        generated.insert("build.rs.template".to_string(), build_rs);
425
426        Ok(generated)
427    }
428
429    fn collect_proto_files(&self) -> anyhow::Result<(Vec<PathBuf>, Vec<PathBuf>)> {
430        let mut proto_files: Vec<PathBuf> = Vec::new();
431        let mut includes: Vec<PathBuf> = Vec::new();
432
433        for path in &self.config.proto_paths {
434            if path.is_dir() {
435                includes.push(path.clone());
436                for entry in std::fs::read_dir(path)? {
437                    let entry = entry?;
438                    let p = entry.path();
439                    if p.extension().is_some_and(|e| e == "proto") {
440                        proto_files.push(p);
441                    }
442                }
443            } else if path.is_file() && path.extension().is_some_and(|e| e == "proto") {
444                proto_files.push(path.clone());
445                if let Some(parent) = path.parent() {
446                    includes.push(parent.to_path_buf());
447                }
448            }
449        }
450
451        proto_files.sort();
452        includes.sort();
453        includes.dedup();
454
455        Ok((proto_files, includes))
456    }
457
458    fn parse_proto_files(proto_files: &[PathBuf]) -> anyhow::Result<Vec<ProtoInfo>> {
459        let mut infos = Vec::new();
460        for pf in proto_files {
461            let content = std::fs::read_to_string(pf)?;
462            let info = parse_proto_content(&content);
463            infos.push(info);
464        }
465        Ok(infos)
466    }
467
468    fn compile_with_tonic(
469        &self,
470        proto_files: &[PathBuf],
471        include_dirs: &[PathBuf],
472    ) -> anyhow::Result<()> {
473        let proto_strs: Vec<&Path> = proto_files.iter().map(PathBuf::as_path).collect();
474
475        let include_strs: Vec<&Path> = include_dirs.iter().map(PathBuf::as_path).collect();
476
477        let mut builder = tonic_prost_build::configure();
478        if let Some(ref out) = self.config.output_dir {
479            builder = builder.out_dir(out);
480        }
481
482        builder.compile_protos(&proto_strs, &include_strs)?;
483
484        Ok(())
485    }
486
487    fn render_dubbo_integration(&self, info: &ProtoInfo) -> anyhow::Result<String> {
488        let mut code = String::new();
489        let package = &info.package;
490        let include_name = package;
491
492        writeln!(code, "// Dubbo integration for `{package}` package.")?;
493        writeln!(code, "// Generated by dubbo-rs-codegen. DO NOT EDIT.")?;
494        writeln!(code)?;
495
496        writeln!(code, "/// Proto types and tonic stubs.")?;
497        writeln!(code, "#[allow(clippy::all, clippy::pedantic)]")?;
498        writeln!(code, "pub mod proto {{")?;
499        writeln!(code, "    tonic::include_proto!(\"{include_name}\");")?;
500        writeln!(code, "}}")?;
501        writeln!(code)?;
502
503        if self.config.enable_server {
504            Self::render_service_registration(&mut code, info)?;
505        }
506
507        if self.config.enable_client {
508            match &self.config.client_mode {
509                ClientMode::Channel | ClientMode::Both => {
510                    Self::render_channel_client(&mut code, info)?;
511                }
512                ClientMode::Invoker => {}
513            }
514            match &self.config.client_mode {
515                ClientMode::Invoker | ClientMode::Both => {
516                    Self::render_invoker_client(&mut code, info)?;
517                }
518                ClientMode::Channel => {}
519            }
520        }
521
522        Ok(code)
523    }
524
525    fn render_service_registration(code: &mut String, info: &ProtoInfo) -> std::fmt::Result {
526        writeln!(code, "// === Service Registration ===")?;
527        writeln!(code, "#[allow(clippy::all, clippy::pedantic)]")?;
528        writeln!(code)?;
529
530        for svc in &info.services {
531            let svc_snake = to_snake_case(&svc.name);
532            let svc_server_mod = format!("{svc_snake}_server");
533            let svc_server_struct = format!("{}Server", svc.name);
534
535            writeln!(
536                code,
537                "/// Register a `{}` service with a Dubbo server.",
538                svc.name
539            )?;
540            writeln!(code, "#[allow(clippy::all, clippy::pedantic)]")?;
541            writeln!(code, "pub fn register_{svc_snake}_service(")?;
542            writeln!(code, "    server: dubbo_rs::server::Server,")?;
543            writeln!(
544                code,
545                "    svc: impl proto::{svc_server_mod}::{name} + 'static,",
546                name = svc.name
547            )?;
548            writeln!(code, ") -> dubbo_rs::server::Server {{")?;
549            writeln!(code, "    server.register_service(|mut builder| {{")?;
550            writeln!(
551                code,
552                "        builder.add_service(proto::{svc_server_mod}::{svc_server_struct}::new(svc))"
553            )?;
554            writeln!(code, "    }})")?;
555            writeln!(code, "}}")?;
556            writeln!(code)?;
557        }
558
559        Ok(())
560    }
561
562    fn render_channel_client(code: &mut String, info: &ProtoInfo) -> std::fmt::Result {
563        writeln!(code, "// === Channel Client ===")?;
564        writeln!(code, "#[allow(clippy::all, clippy::pedantic)]")?;
565        writeln!(code)?;
566
567        for svc in &info.services {
568            let svc_snake = to_snake_case(&svc.name);
569            let client_struct = format!("{}ChannelClient", svc.name);
570            let tonic_client_mod = format!("{svc_snake}_client");
571            let tonic_client_struct = format!("{}Client", svc.name);
572
573            writeln!(
574                code,
575                "/// Channel-based Dubbo client for the `{}` service.",
576                svc.name
577            )?;
578            writeln!(code, "#[allow(clippy::all, clippy::pedantic)]")?;
579            writeln!(code, "pub struct {client_struct} {{")?;
580            writeln!(
581                code,
582                "    inner: proto::{tonic_client_mod}::{tonic_client_struct}<tonic::transport::Channel>,"
583            )?;
584            writeln!(code, "}}")?;
585            writeln!(code)?;
586
587            writeln!(code, "#[allow(clippy::all, clippy::pedantic)]")?;
588            writeln!(code, "impl {client_struct} {{")?;
589            writeln!(
590                code,
591                "    /// Connect to the service at the given endpoint."
592            )?;
593            writeln!(
594                code,
595                "    pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>"
596            )?;
597            writeln!(code, "    where")?;
598            writeln!(code, "        D: TryInto<tonic::transport::Endpoint>,")?;
599            writeln!(
600                code,
601                "        <D as TryInto<tonic::transport::Endpoint>>::Error:"
602            )?;
603            writeln!(
604                code,
605                "            Into<Box<dyn std::error::Error + Send + Sync>>,"
606            )?;
607            writeln!(code, "    {{")?;
608            writeln!(
609                code,
610                "        let inner = proto::{tonic_client_mod}::{tonic_client_struct}::connect(dst).await?;"
611            )?;
612            writeln!(code, "        Ok(Self {{ inner }})")?;
613            writeln!(code, "    }}")?;
614            writeln!(code)?;
615
616            writeln!(code, "    /// Create from an existing tonic channel.")?;
617            writeln!(
618                code,
619                "    pub fn from_channel(channel: tonic::transport::Channel) -> Self {{"
620            )?;
621            writeln!(code, "        Self {{")?;
622            writeln!(
623                code,
624                "            inner: proto::{tonic_client_mod}::{tonic_client_struct}::new(channel),"
625            )?;
626            writeln!(code, "        }}")?;
627            writeln!(code, "    }}")?;
628            writeln!(code)?;
629
630            writeln!(code, "    /// Create from a Dubbo `Client`'s channel.")?;
631            writeln!(
632                code,
633                "    pub fn from_dubbo_client(client: &dubbo_rs::client::Client) -> Option<Self> {{"
634            )?;
635            writeln!(
636                code,
637                "        client.channel().cloned().map(Self::from_channel)"
638            )?;
639            writeln!(code, "    }}")?;
640            writeln!(code, "}}")?;
641            writeln!(code)?;
642
643            // RPC methods
644            writeln!(code, "#[allow(clippy::all, clippy::pedantic)]")?;
645            writeln!(code, "impl {client_struct} {{")?;
646            for method in &svc.methods {
647                Self::render_channel_method(code, info, svc, method)?;
648            }
649            writeln!(code, "}}")?;
650            writeln!(code)?;
651        }
652
653        Ok(())
654    }
655
656    fn render_channel_method(
657        code: &mut String,
658        _info: &ProtoInfo,
659        _svc: &ProtoService,
660        method: &ProtoMethod,
661    ) -> std::fmt::Result {
662        let method_snake = to_snake_case(&method.name);
663
664        if method.server_streaming {
665            writeln!(code, "    pub async fn {method_snake}(")?;
666            writeln!(code, "        &mut self,")?;
667            writeln!(
668                code,
669                "        request: impl tonic::IntoRequest<proto::{}>,",
670                method.input_type
671            )?;
672            writeln!(
673                code,
674                "    ) -> Result<tonic::Response<tonic::codec::Streaming<proto::{}>>, tonic::Status> {{",
675                method.output_type
676            )?;
677            writeln!(code, "        self.inner.{method_snake}(request).await")?;
678            writeln!(code, "    }}")?;
679        } else if method.client_streaming && !method.server_streaming {
680            writeln!(code, "    pub async fn {method_snake}(")?;
681            writeln!(code, "        &mut self,")?;
682            writeln!(
683                code,
684                "        request: impl tonic::IntoStreamingRequest<Message = proto::{}>,",
685                method.input_type
686            )?;
687            writeln!(
688                code,
689                "    ) -> Result<tonic::Response<proto::{}>, tonic::Status> {{",
690                method.output_type
691            )?;
692            writeln!(code, "        self.inner.{method_snake}(request).await")?;
693            writeln!(code, "    }}")?;
694        } else if method.client_streaming && method.server_streaming {
695            // Bidi streaming
696            writeln!(code, "    pub async fn {method_snake}(")?;
697            writeln!(code, "        &mut self,")?;
698            writeln!(
699                code,
700                "        request: impl tonic::IntoStreamingRequest<Message = proto::{}>,",
701                method.input_type
702            )?;
703            writeln!(
704                code,
705                "    ) -> Result<tonic::Response<tonic::codec::Streaming<proto::{}>>, tonic::Status> {{",
706                method.output_type
707            )?;
708            writeln!(code, "        self.inner.{method_snake}(request).await")?;
709            writeln!(code, "    }}")?;
710        } else {
711            // Unary
712            writeln!(code, "    pub async fn {method_snake}(")?;
713            writeln!(code, "        &mut self,")?;
714            writeln!(
715                code,
716                "        request: impl tonic::IntoRequest<proto::{}>,",
717                method.input_type
718            )?;
719            writeln!(
720                code,
721                "    ) -> Result<tonic::Response<proto::{}>, tonic::Status> {{",
722                method.output_type
723            )?;
724            writeln!(code, "        self.inner.{method_snake}(request).await")?;
725            writeln!(code, "    }}")?;
726        }
727
728        Ok(())
729    }
730
731    fn render_invoker_client(code: &mut String, info: &ProtoInfo) -> std::fmt::Result {
732        writeln!(code, "// === Invoker Client ===")?;
733        writeln!(code, "#[allow(clippy::all, clippy::pedantic)]")?;
734        writeln!(code)?;
735
736        let package = &info.package;
737
738        for svc in &info.services {
739            let invoker_struct = format!("{}InvokerClient", svc.name);
740
741            writeln!(
742                code,
743                "/// Invoker-based Dubbo client for the `{}` service.",
744                svc.name
745            )?;
746            writeln!(code, "#[allow(clippy::all, clippy::pedantic)]")?;
747            writeln!(code, "pub struct {invoker_struct} {{")?;
748            writeln!(code, "    invoker: Box<dyn dubbo_rs::protocol::Invoker>,")?;
749            writeln!(code, "}}")?;
750            writeln!(code)?;
751
752            writeln!(code, "#[allow(clippy::all, clippy::pedantic)]")?;
753            writeln!(code, "impl {invoker_struct} {{")?;
754            writeln!(code, "    /// Create a new client with the given invoker.")?;
755            writeln!(
756                code,
757                "    pub fn new(invoker: Box<dyn dubbo_rs::protocol::Invoker>) -> Self {{"
758            )?;
759            writeln!(code, "        Self {{ invoker }}")?;
760            writeln!(code, "    }}")?;
761            writeln!(code)?;
762
763            writeln!(code, "    /// Get a reference to the underlying invoker.")?;
764            writeln!(
765                code,
766                "    pub fn invoker(&self) -> &dyn dubbo_rs::protocol::Invoker {{"
767            )?;
768            writeln!(code, "        self.invoker.as_ref()")?;
769            writeln!(code, "    }}")?;
770            writeln!(code, "}}")?;
771            writeln!(code)?;
772
773            // RPC methods
774            writeln!(code, "#[allow(clippy::all, clippy::pedantic)]")?;
775            writeln!(code, "impl {invoker_struct} {{")?;
776            for method in &svc.methods {
777                Self::render_invoker_method(code, package, svc, method)?;
778            }
779            writeln!(code, "}}")?;
780            writeln!(code)?;
781        }
782
783        Ok(())
784    }
785
786    fn render_invoker_method(
787        code: &mut String,
788        package: &str,
789        svc: &ProtoService,
790        method: &ProtoMethod,
791    ) -> std::fmt::Result {
792        let method_snake = to_snake_case(&method.name);
793        let method_path = format!(
794            "/{package}.{svc_name}/{method_name}",
795            svc_name = svc.name,
796            method_name = method.name
797        );
798
799        if method.client_streaming || method.server_streaming {
800            let stream_type = if method.client_streaming && method.server_streaming {
801                "Bidirectional streaming"
802            } else if method.server_streaming {
803                "Server streaming"
804            } else {
805                "Client streaming"
806            };
807            let client_name = format!("{}ChannelClient", svc.name);
808            writeln!(
809                code,
810                "    // Note: {stream_type} is not supported via Invoker."
811            )?;
812            writeln!(
813                code,
814                "    // Use {client_name} instead for streaming methods."
815            )?;
816            writeln!(code)?;
817            return Ok(());
818        }
819
820        // Unary method
821        writeln!(code, "    pub async fn {method_snake}(")?;
822        writeln!(code, "        &self,")?;
823        writeln!(code, "        request: proto::{},", method.input_type)?;
824        writeln!(
825            code,
826            "    ) -> anyhow::Result<proto::{}> {{",
827            method.output_type
828        )?;
829        writeln!(
830            code,
831            "        let mut ctx = dubbo_rs::protocol::InvocationContext::new("
832        )?;
833        writeln!(code, "            \"{method_path}\",")?;
834        writeln!(code, "            self.invoker.get_url().clone(),")?;
835        writeln!(code, "        );")?;
836        writeln!(
837            code,
838            "        ctx.arguments = vec![prost::Message::encode_to_vec(&request)];"
839        )?;
840        writeln!(
841            code,
842            "        let result = self.invoker.invoke(&mut ctx).await?;"
843        )?;
844        writeln!(code, "        let value = result.value")?;
845        writeln!(
846            code,
847            "            .ok_or_else(|| anyhow::anyhow!(\"empty response from {method_name}\"))?;",
848            method_name = method.name
849        )?;
850        writeln!(code, "        Ok(prost::Message::decode(&value[..])?)")?;
851        writeln!(code, "    }}")?;
852
853        Ok(())
854    }
855
856    fn render_build_rs_template(proto_files: &[PathBuf], include_dirs: &[PathBuf]) -> String {
857        let mut code = String::new();
858
859        let proto_strs: Vec<String> = proto_files
860            .iter()
861            .filter_map(|p| p.to_str().map(String::from))
862            .collect();
863        let include_strs: Vec<String> = include_dirs
864            .iter()
865            .filter_map(|p| p.to_str().map(String::from))
866            .collect();
867
868        let _ = writeln!(code, "fn main() -> anyhow::Result<()> {{");
869        let _ = writeln!(code, "    // Compile proto files with tonic-prost-build");
870        let _ = writeln!(code, "    tonic_prost_build::compile_protos(");
871        let _ = writeln!(code, "        &[");
872
873        for ps in &proto_strs {
874            let _ = writeln!(code, "            \"{ps}\",");
875        }
876
877        let _ = writeln!(code, "        ],");
878        let _ = writeln!(code, "        &[");
879
880        for inc in &include_strs {
881            let _ = writeln!(code, "            \"{inc}\",");
882        }
883
884        let _ = writeln!(code, "        ],");
885        let _ = writeln!(code, "    )?;");
886
887        let _ = writeln!(code);
888        let _ = writeln!(code, "    // Optional: generate Dubbo integration wrappers");
889        let _ = writeln!(
890            code,
891            "    // let config = dubbo_rs_codegen::GeneratorConfigBuilder::new()"
892        );
893        for ps in &proto_strs {
894            let _ = writeln!(code, "    //     .proto_path(\"{ps}\")");
895        }
896        let _ = writeln!(code, "    //     .build()?;");
897        let _ = writeln!(
898            code,
899            "    // let generator = dubbo_rs_codegen::CodeGenerator::new(config);"
900        );
901        let _ = writeln!(code, "    // let generated = generator.generate()?;");
902        let _ = writeln!(
903            code,
904            "    // generated.write_to_dir(std::path::Path::new(\"src/gen\"))?;"
905        );
906
907        let _ = writeln!(code);
908        let _ = writeln!(code, "    Ok(())");
909        let _ = writeln!(code, "}}");
910
911        code
912    }
913
914    // -----------------------------------------------------------------------
915    // Exposed for testing — parse a single proto string
916    // -----------------------------------------------------------------------
917
918    #[cfg(test)]
919    fn parse_proto_string(content: &str) -> ProtoInfo {
920        parse_proto_content(content)
921    }
922}
923
924// ---------------------------------------------------------------------------
925// Tests
926// ---------------------------------------------------------------------------
927
928#[cfg(test)]
929mod tests {
930    use super::*;
931
932    // === Config / Builder tests ===
933
934    #[test]
935    fn test_builder_defaults() {
936        let config = GeneratorConfig::default();
937        assert_eq!(config.proto_paths.len(), 1);
938        assert!(config.enable_client);
939        assert!(config.enable_server);
940        assert!(config.output_dir.is_some());
941        assert_eq!(config.client_mode, ClientMode::Both);
942    }
943
944    #[test]
945    fn test_builder_with_params() {
946        let config = GeneratorConfigBuilder::new()
947            .proto_path("api")
948            .proto_path("shared")
949            .output_dir("src/generated")
950            .enable_client(false)
951            .build()
952            .unwrap();
953
954        assert_eq!(config.proto_paths.len(), 2);
955        assert!(!config.enable_client);
956        assert!(config.enable_server);
957    }
958
959    #[test]
960    fn test_builder_empty_proto_paths_fails() {
961        let result = GeneratorConfigBuilder::new().build();
962        assert!(result.is_err());
963        assert!(result.unwrap_err().to_string().contains("proto_paths"));
964    }
965
966    #[test]
967    fn test_builder_client_mode() {
968        let config = GeneratorConfigBuilder::new()
969            .proto_path("proto")
970            .client_mode(ClientMode::Channel)
971            .build()
972            .unwrap();
973        assert_eq!(config.client_mode, ClientMode::Channel);
974    }
975
976    #[test]
977    fn test_client_mode_default() {
978        assert_eq!(ClientMode::default(), ClientMode::Both);
979    }
980
981    // === GeneratedCode tests ===
982
983    #[test]
984    fn test_generated_code_empty() {
985        let generated = GeneratedCode::new();
986        assert!(generated.is_empty());
987    }
988
989    #[test]
990    fn test_generated_code_insert() {
991        let mut generated = GeneratedCode::new();
992        generated.insert("svc.rs".into(), "// generated".into());
993        assert!(!generated.is_empty());
994        assert_eq!(generated.files.len(), 1);
995    }
996
997    #[test]
998    fn test_code_generator_creation() {
999        let config = GeneratorConfig::default();
1000        let cg = CodeGenerator::new(config);
1001        assert!(cg.config.enable_client);
1002    }
1003
1004    #[test]
1005    fn test_generated_code_write_to_dir() {
1006        let mut generated = GeneratedCode::new();
1007        generated.insert("test.rs".into(), "fn test() {}".into());
1008
1009        let dir = std::env::temp_dir().join(format!("dubbo-rs-codegen-test-{}", std::process::id()));
1010        generated.write_to_dir(&dir).unwrap();
1011        assert!(dir.join("test.rs").exists());
1012
1013        std::fs::remove_dir_all(&dir).ok();
1014    }
1015
1016    // === Pascal case / snake case tests ===
1017
1018    #[test]
1019    fn test_to_pascal_case_conversion() {
1020        assert_eq!(to_pascal_case("greeter"), "Greeter");
1021        assert_eq!(to_pascal_case("user_service"), "UserService");
1022        assert_eq!(to_pascal_case("helloworld"), "Helloworld");
1023        assert_eq!(to_pascal_case("my_complex_service"), "MyComplexService");
1024        assert_eq!(to_pascal_case("a"), "A");
1025        assert_eq!(to_pascal_case(""), "");
1026    }
1027
1028    #[test]
1029    fn test_to_snake_case_conversion() {
1030        assert_eq!(to_snake_case("SayHello"), "say_hello");
1031        assert_eq!(to_snake_case("Greeter"), "greeter");
1032        assert_eq!(to_snake_case("TelephoneExchange"), "telephone_exchange");
1033        assert_eq!(to_snake_case("BidiStreamEcho"), "bidi_stream_echo");
1034        assert_eq!(to_snake_case("Echo"), "echo");
1035        assert_eq!(to_snake_case("RPC"), "rpc");
1036        assert_eq!(to_snake_case("GetUserByID"), "get_user_by_id");
1037    }
1038
1039    // === Proto parser tests ===
1040
1041    #[test]
1042    fn test_parse_unary_proto() {
1043        let proto = r#"
1044            syntax = "proto3";
1045            package greeter;
1046            service Greeter {
1047              rpc SayHello (HelloRequest) returns (HelloReply);
1048            }
1049        "#;
1050        let info = CodeGenerator::parse_proto_string(proto);
1051        assert_eq!(info.package, "greeter");
1052        assert_eq!(info.services.len(), 1);
1053        assert_eq!(info.services[0].name, "Greeter");
1054        assert_eq!(info.services[0].methods.len(), 1);
1055        assert_eq!(info.services[0].methods[0].name, "SayHello");
1056        assert_eq!(info.services[0].methods[0].input_type, "HelloRequest");
1057        assert_eq!(info.services[0].methods[0].output_type, "HelloReply");
1058        assert!(!info.services[0].methods[0].client_streaming);
1059        assert!(!info.services[0].methods[0].server_streaming);
1060    }
1061
1062    #[test]
1063    fn test_parse_server_streaming_proto() {
1064        let proto = r#"
1065            syntax = "proto3";
1066            package exchange;
1067            service TelephoneExchange {
1068              rpc Dial(DialRequest) returns (stream DialProgress);
1069            }
1070        "#;
1071        let info = CodeGenerator::parse_proto_string(proto);
1072        assert_eq!(info.services[0].methods[0].name, "Dial");
1073        assert!(info.services[0].methods[0].server_streaming);
1074        assert!(!info.services[0].methods[0].client_streaming);
1075    }
1076
1077    #[test]
1078    fn test_parse_all_rpc_types() {
1079        let proto = r#"
1080            syntax = "proto3";
1081            package triple.test;
1082            service TripleService {
1083              rpc Echo(EchoRequest) returns (EchoResponse);
1084              rpc ServerStreamEcho(EchoRequest) returns (stream EchoResponse);
1085              rpc ClientStreamEcho(stream EchoRequest) returns (EchoResponse);
1086              rpc BidiStreamEcho(stream EchoRequest) returns (stream EchoResponse);
1087            }
1088        "#;
1089        let info = CodeGenerator::parse_proto_string(proto);
1090        assert_eq!(info.package, "triple.test");
1091        let methods = &info.services[0].methods;
1092        assert_eq!(methods.len(), 4);
1093
1094        assert_eq!(methods[0].name, "Echo");
1095        assert!(!methods[0].client_streaming);
1096        assert!(!methods[0].server_streaming);
1097
1098        assert_eq!(methods[1].name, "ServerStreamEcho");
1099        assert!(!methods[1].client_streaming);
1100        assert!(methods[1].server_streaming);
1101
1102        assert_eq!(methods[2].name, "ClientStreamEcho");
1103        assert!(methods[2].client_streaming);
1104        assert!(!methods[2].server_streaming);
1105
1106        assert_eq!(methods[3].name, "BidiStreamEcho");
1107        assert!(methods[3].client_streaming);
1108        assert!(methods[3].server_streaming);
1109    }
1110
1111    #[test]
1112    fn test_parse_with_comments() {
1113        let proto = r#"
1114            syntax = "proto3";
1115            // This is a comment
1116            package test;
1117            /* Multi-line
1118               comment */
1119            service Greeter {
1120              // Sends a greeting
1121              rpc SayHello (HelloRequest) returns (HelloReply);
1122            }
1123        "#;
1124        let info = CodeGenerator::parse_proto_string(proto);
1125        assert_eq!(info.package, "test");
1126        assert_eq!(info.services[0].methods.len(), 1);
1127    }
1128
1129    #[test]
1130    fn test_parse_no_services() {
1131        let proto = r#"
1132            syntax = "proto3";
1133            package messages;
1134            message Empty {}
1135        "#;
1136        let info = CodeGenerator::parse_proto_string(proto);
1137        assert_eq!(info.package, "messages");
1138        assert!(info.services.is_empty());
1139    }
1140
1141    #[test]
1142    fn test_parse_multiple_services() {
1143        let proto = r#"
1144            syntax = "proto3";
1145            package multi;
1146            service ServiceA {
1147              rpc MethodA(Req) returns (Res);
1148            }
1149            service ServiceB {
1150              rpc MethodB(Req) returns (Res);
1151            }
1152        "#;
1153        let info = CodeGenerator::parse_proto_string(proto);
1154        assert_eq!(info.services.len(), 2);
1155        assert_eq!(info.services[0].name, "ServiceA");
1156        assert_eq!(info.services[1].name, "ServiceB");
1157    }
1158
1159    #[test]
1160    fn test_parse_nested_package() {
1161        let proto = r#"
1162            syntax = "proto3";
1163            package triple.test;
1164            service TripleService {
1165              rpc Echo(Req) returns (Res);
1166            }
1167        "#;
1168        let info = CodeGenerator::parse_proto_string(proto);
1169        assert_eq!(info.package, "triple.test");
1170    }
1171
1172    // === Service registration renderer tests ===
1173
1174    #[test]
1175    fn test_render_service_registration_unary() {
1176        let info = ProtoInfo {
1177            package: "greeter".to_string(),
1178            services: vec![ProtoService {
1179                name: "Greeter".to_string(),
1180                methods: vec![ProtoMethod {
1181                    name: "SayHello".to_string(),
1182                    input_type: "HelloRequest".to_string(),
1183                    output_type: "HelloReply".to_string(),
1184                    client_streaming: false,
1185                    server_streaming: false,
1186                }],
1187            }],
1188        };
1189
1190        let config = GeneratorConfig {
1191            proto_paths: vec![PathBuf::from("proto")],
1192            output_dir: None,
1193            enable_client: false,
1194            enable_server: true,
1195            client_mode: ClientMode::Both,
1196        };
1197        let generator = CodeGenerator::new(config);
1198        let code = generator.render_dubbo_integration(&info).unwrap();
1199
1200        assert!(code.contains("pub mod proto"));
1201        assert!(code.contains("tonic::include_proto!(\"greeter\")"));
1202        assert!(code.contains("pub fn register_greeter_service"));
1203        assert!(code.contains("svc: impl proto::greeter_server::Greeter + 'static"));
1204        assert!(
1205            code.contains("builder.add_service(proto::greeter_server::GreeterServer::new(svc))")
1206        );
1207    }
1208
1209    #[test]
1210    fn test_render_service_registration_streaming() {
1211        let info = ProtoInfo {
1212            package: "exchange".to_string(),
1213            services: vec![ProtoService {
1214                name: "TelephoneExchange".to_string(),
1215                methods: vec![ProtoMethod {
1216                    name: "Dial".to_string(),
1217                    input_type: "DialRequest".to_string(),
1218                    output_type: "DialProgress".to_string(),
1219                    client_streaming: false,
1220                    server_streaming: true,
1221                }],
1222            }],
1223        };
1224
1225        let config = GeneratorConfig {
1226            proto_paths: vec![PathBuf::from("proto")],
1227            output_dir: None,
1228            enable_client: false,
1229            enable_server: true,
1230            client_mode: ClientMode::Both,
1231        };
1232        let generator = CodeGenerator::new(config);
1233        let code = generator.render_dubbo_integration(&info).unwrap();
1234
1235        assert!(code.contains("register_telephone_exchange_service"));
1236        assert!(code.contains("impl proto::telephone_exchange_server::TelephoneExchange + 'static"));
1237    }
1238
1239    // === Channel client renderer tests ===
1240
1241    #[test]
1242    fn test_render_channel_client_unary() {
1243        let info = ProtoInfo {
1244            package: "greeter".to_string(),
1245            services: vec![ProtoService {
1246                name: "Greeter".to_string(),
1247                methods: vec![ProtoMethod {
1248                    name: "SayHello".to_string(),
1249                    input_type: "HelloRequest".to_string(),
1250                    output_type: "HelloReply".to_string(),
1251                    client_streaming: false,
1252                    server_streaming: false,
1253                }],
1254            }],
1255        };
1256
1257        let config = GeneratorConfig {
1258            proto_paths: vec![PathBuf::from("proto")],
1259            output_dir: None,
1260            enable_client: true,
1261            enable_server: false,
1262            client_mode: ClientMode::Channel,
1263        };
1264        let generator = CodeGenerator::new(config);
1265        let code = generator.render_dubbo_integration(&info).unwrap();
1266
1267        assert!(code.contains("pub struct GreeterChannelClient"));
1268        assert!(
1269            code.contains("inner: proto::greeter_client::GreeterClient<tonic::transport::Channel>")
1270        );
1271        assert!(code.contains("pub async fn connect"));
1272        assert!(code.contains("pub fn from_channel"));
1273        assert!(code.contains("pub fn from_dubbo_client"));
1274        assert!(code.contains("pub async fn say_hello"));
1275        assert!(code.contains("impl tonic::IntoRequest<proto::HelloRequest>"));
1276        assert!(code.contains("Result<tonic::Response<proto::HelloReply>, tonic::Status>"));
1277    }
1278
1279    #[test]
1280    fn test_render_channel_client_streaming() {
1281        let info = ProtoInfo {
1282            package: "exchange".to_string(),
1283            services: vec![ProtoService {
1284                name: "TelephoneExchange".to_string(),
1285                methods: vec![ProtoMethod {
1286                    name: "Dial".to_string(),
1287                    input_type: "DialRequest".to_string(),
1288                    output_type: "DialProgress".to_string(),
1289                    client_streaming: false,
1290                    server_streaming: true,
1291                }],
1292            }],
1293        };
1294
1295        let config = GeneratorConfig {
1296            proto_paths: vec![PathBuf::from("proto")],
1297            output_dir: None,
1298            enable_client: true,
1299            enable_server: false,
1300            client_mode: ClientMode::Channel,
1301        };
1302        let generator = CodeGenerator::new(config);
1303        let code = generator.render_dubbo_integration(&info).unwrap();
1304
1305        assert!(code.contains("TelephoneExchangeChannelClient"));
1306        assert!(code.contains("pub async fn dial"));
1307        assert!(code.contains("tonic::codec::Streaming<proto::DialProgress>"));
1308    }
1309
1310    // === Invoker client renderer tests ===
1311
1312    #[test]
1313    fn test_render_invoker_client_unary() {
1314        let info = ProtoInfo {
1315            package: "greeter".to_string(),
1316            services: vec![ProtoService {
1317                name: "Greeter".to_string(),
1318                methods: vec![ProtoMethod {
1319                    name: "SayHello".to_string(),
1320                    input_type: "HelloRequest".to_string(),
1321                    output_type: "HelloReply".to_string(),
1322                    client_streaming: false,
1323                    server_streaming: false,
1324                }],
1325            }],
1326        };
1327
1328        let config = GeneratorConfig {
1329            proto_paths: vec![PathBuf::from("proto")],
1330            output_dir: None,
1331            enable_client: true,
1332            enable_server: false,
1333            client_mode: ClientMode::Invoker,
1334        };
1335        let generator = CodeGenerator::new(config);
1336        let code = generator.render_dubbo_integration(&info).unwrap();
1337
1338        assert!(code.contains("pub struct GreeterInvokerClient"));
1339        assert!(code.contains("invoker: Box<dyn dubbo_rs::protocol::Invoker>"));
1340        assert!(code.contains("pub fn new(invoker: Box<dyn dubbo_rs::protocol::Invoker>) -> Self"));
1341        assert!(code.contains("pub async fn say_hello"));
1342        assert!(code.contains("request: proto::HelloRequest"));
1343        assert!(code.contains("prost::Message::encode_to_vec(&request)"));
1344        assert!(code.contains("prost::Message::decode(&value[..])"));
1345        assert!(code.contains("/greeter.Greeter/SayHello"));
1346    }
1347
1348    #[test]
1349    fn test_render_invoker_client_streaming_not_supported() {
1350        let info = ProtoInfo {
1351            package: "exchange".to_string(),
1352            services: vec![ProtoService {
1353                name: "TelephoneExchange".to_string(),
1354                methods: vec![ProtoMethod {
1355                    name: "Dial".to_string(),
1356                    input_type: "DialRequest".to_string(),
1357                    output_type: "DialProgress".to_string(),
1358                    client_streaming: false,
1359                    server_streaming: true,
1360                }],
1361            }],
1362        };
1363
1364        let config = GeneratorConfig {
1365            proto_paths: vec![PathBuf::from("proto")],
1366            output_dir: None,
1367            enable_client: true,
1368            enable_server: false,
1369            client_mode: ClientMode::Invoker,
1370        };
1371        let generator = CodeGenerator::new(config);
1372        let code = generator.render_dubbo_integration(&info).unwrap();
1373
1374        assert!(code.contains("not supported via Invoker"));
1375        assert!(code.contains("TelephoneExchangeChannelClient"));
1376    }
1377
1378    // === Full RPC types integration test ===
1379
1380    #[test]
1381    fn test_render_all_rpc_types() {
1382        let info = ProtoInfo {
1383            package: "triple.test".to_string(),
1384            services: vec![ProtoService {
1385                name: "TripleService".to_string(),
1386                methods: vec![
1387                    ProtoMethod {
1388                        name: "Echo".to_string(),
1389                        input_type: "EchoRequest".to_string(),
1390                        output_type: "EchoResponse".to_string(),
1391                        client_streaming: false,
1392                        server_streaming: false,
1393                    },
1394                    ProtoMethod {
1395                        name: "ServerStreamEcho".to_string(),
1396                        input_type: "EchoRequest".to_string(),
1397                        output_type: "EchoResponse".to_string(),
1398                        client_streaming: false,
1399                        server_streaming: true,
1400                    },
1401                    ProtoMethod {
1402                        name: "ClientStreamEcho".to_string(),
1403                        input_type: "EchoRequest".to_string(),
1404                        output_type: "EchoResponse".to_string(),
1405                        client_streaming: true,
1406                        server_streaming: false,
1407                    },
1408                    ProtoMethod {
1409                        name: "BidiStreamEcho".to_string(),
1410                        input_type: "EchoRequest".to_string(),
1411                        output_type: "EchoResponse".to_string(),
1412                        client_streaming: true,
1413                        server_streaming: true,
1414                    },
1415                ],
1416            }],
1417        };
1418
1419        let config = GeneratorConfig {
1420            proto_paths: vec![PathBuf::from("proto")],
1421            output_dir: None,
1422            enable_client: true,
1423            enable_server: true,
1424            client_mode: ClientMode::Both,
1425        };
1426        let generator = CodeGenerator::new(config);
1427        let code = generator.render_dubbo_integration(&info).unwrap();
1428
1429        // Check module include
1430        assert!(code.contains("tonic::include_proto!(\"triple.test\")"));
1431
1432        // Check registration
1433        assert!(code.contains("register_triple_service_service"));
1434
1435        // Check channel client methods
1436        assert!(code.contains("pub async fn echo"));
1437        assert!(code.contains("pub async fn server_stream_echo"));
1438        assert!(code.contains("pub async fn client_stream_echo"));
1439        assert!(code.contains("pub async fn bidi_stream_echo"));
1440
1441        // Check streaming return types
1442        assert!(code.contains("tonic::codec::Streaming<proto::EchoResponse>"));
1443
1444        // Check invoker client: unary has method, streaming has comment
1445        assert!(code.contains("/triple.test.TripleService/Echo"));
1446        assert!(code.contains("not supported via Invoker"));
1447    }
1448
1449    // === Nested package test ===
1450
1451    #[test]
1452    fn test_nested_package_module_name() {
1453        let info = ProtoInfo {
1454            package: "triple.test".to_string(),
1455            services: vec![ProtoService {
1456                name: "TripleService".to_string(),
1457                methods: vec![ProtoMethod {
1458                    name: "Echo".to_string(),
1459                    input_type: "Req".to_string(),
1460                    output_type: "Res".to_string(),
1461                    client_streaming: false,
1462                    server_streaming: false,
1463                }],
1464            }],
1465        };
1466
1467        let config = GeneratorConfig {
1468            proto_paths: vec![PathBuf::from("proto")],
1469            output_dir: None,
1470            enable_client: true,
1471            enable_server: true,
1472            client_mode: ClientMode::Both,
1473        };
1474        let generator = CodeGenerator::new(config);
1475        let code = generator.render_dubbo_integration(&info).unwrap();
1476
1477        assert!(code.contains("tonic::include_proto!(\"triple.test\")"));
1478        assert!(code.contains("proto::triple_service_server::TripleService"));
1479        assert!(code.contains("proto::triple_service_client::TripleServiceClient"));
1480    }
1481
1482    // === Build.rs template test ===
1483
1484    #[test]
1485    fn test_render_build_rs_template() {
1486        let _generator = CodeGenerator::new(GeneratorConfig::default());
1487        let template = CodeGenerator::render_build_rs_template(
1488            &[PathBuf::from("proto/greeter.proto")],
1489            &[PathBuf::from("proto")],
1490        );
1491        assert!(template.contains("tonic_prost_build::compile_protos"));
1492        assert!(template.contains("proto/greeter.proto"));
1493        assert!(template.contains("dubbo_rs_codegen"));
1494    }
1495
1496    // === No services produces no output ===
1497
1498    #[test]
1499    fn test_render_no_services() {
1500        let info = ProtoInfo {
1501            package: "messages".to_string(),
1502            services: vec![],
1503        };
1504
1505        let config = GeneratorConfig {
1506            proto_paths: vec![PathBuf::from("proto")],
1507            output_dir: None,
1508            enable_client: true,
1509            enable_server: true,
1510            client_mode: ClientMode::Both,
1511        };
1512        let generator = CodeGenerator::new(config);
1513        let code = generator.render_dubbo_integration(&info).unwrap();
1514
1515        // Should still have the proto module but no service/client sections
1516        assert!(code.contains("pub mod proto"));
1517        assert!(!code.contains("register_"));
1518        assert!(!code.contains("ChannelClient"));
1519        assert!(!code.contains("InvokerClient"));
1520    }
1521
1522    // === Edge case: method name casing ===
1523
1524    #[test]
1525    fn test_method_name_snake_case_in_channel() {
1526        let info = ProtoInfo {
1527            package: "test".to_string(),
1528            services: vec![ProtoService {
1529                name: "TestService".to_string(),
1530                methods: vec![ProtoMethod {
1531                    name: "GetUserByID".to_string(),
1532                    input_type: "Req".to_string(),
1533                    output_type: "Res".to_string(),
1534                    client_streaming: false,
1535                    server_streaming: false,
1536                }],
1537            }],
1538        };
1539
1540        let config = GeneratorConfig {
1541            proto_paths: vec![PathBuf::from("proto")],
1542            output_dir: None,
1543            enable_client: true,
1544            enable_server: false,
1545            client_mode: ClientMode::Channel,
1546        };
1547        let generator = CodeGenerator::new(config);
1548        let code = generator.render_dubbo_integration(&info).unwrap();
1549
1550        assert!(code.contains("pub async fn get_user_by_id"));
1551        // Invoker path keeps original casing
1552    }
1553
1554    #[test]
1555    fn test_invoker_method_path_casing() {
1556        let info = ProtoInfo {
1557            package: "greeter".to_string(),
1558            services: vec![ProtoService {
1559                name: "Greeter".to_string(),
1560                methods: vec![ProtoMethod {
1561                    name: "SayHello".to_string(),
1562                    input_type: "HelloRequest".to_string(),
1563                    output_type: "HelloReply".to_string(),
1564                    client_streaming: false,
1565                    server_streaming: false,
1566                }],
1567            }],
1568        };
1569
1570        let config = GeneratorConfig {
1571            proto_paths: vec![PathBuf::from("proto")],
1572            output_dir: None,
1573            enable_client: true,
1574            enable_server: false,
1575            client_mode: ClientMode::Invoker,
1576        };
1577        let generator = CodeGenerator::new(config);
1578        let code = generator.render_dubbo_integration(&info).unwrap();
1579
1580        // Method path uses original proto casing
1581        assert!(code.contains("/greeter.Greeter/SayHello"));
1582    }
1583}