1pub use serde;
2
3use std::collections::HashMap;
4use std::fmt::Write;
5use std::path::{Path, PathBuf};
6
7#[derive(Clone, Debug, Default, PartialEq, Eq)]
13pub enum ClientMode {
14 Channel,
16 Invoker,
18 #[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 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 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
150struct 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#[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
211fn 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 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 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 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
377pub 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 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 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 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 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 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 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 #[cfg(test)]
919 fn parse_proto_string(content: &str) -> ProtoInfo {
920 parse_proto_content(content)
921 }
922}
923
924#[cfg(test)]
929mod tests {
930 use super::*;
931
932 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 assert!(code.contains("tonic::include_proto!(\"triple.test\")"));
1431
1432 assert!(code.contains("register_triple_service_service"));
1434
1435 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 assert!(code.contains("tonic::codec::Streaming<proto::EchoResponse>"));
1443
1444 assert!(code.contains("/triple.test.TripleService/Echo"));
1446 assert!(code.contains("not supported via Invoker"));
1447 }
1448
1449 #[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 #[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 #[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 assert!(code.contains("pub mod proto"));
1517 assert!(!code.contains("register_"));
1518 assert!(!code.contains("ChannelClient"));
1519 assert!(!code.contains("InvokerClient"));
1520 }
1521
1522 #[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 }
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 assert!(code.contains("/greeter.Greeter/SayHello"));
1582 }
1583}