1use std::collections::HashMap;
4
5use crate::features::{self, ResolvedFeatures};
6use crate::generated::descriptor::{DescriptorProto, EnumDescriptorProto, FileDescriptorProto};
7use crate::oneof::to_snake_case;
8use crate::CodeGenConfig;
9
10pub struct CodeGenContext<'a> {
16 pub files: &'a [FileDescriptorProto],
18 pub config: &'a CodeGenConfig,
20 pub type_map: HashMap<String, String>,
26 package_of: HashMap<String, String>,
31 enum_closedness: HashMap<String, bool>,
39}
40
41impl<'a> CodeGenContext<'a> {
42 pub fn new(
48 files: &'a [FileDescriptorProto],
49 config: &'a CodeGenConfig,
50 effective_extern_paths: &[(String, String)],
51 ) -> Self {
52 let mut type_map = HashMap::new();
53 let mut package_of = HashMap::new();
54 let mut enum_closedness = HashMap::new();
55
56 for file in files {
57 let package = file.package.as_deref().unwrap_or("");
58 let file_features = features::for_file(file);
59 let proto_prefix = if package.is_empty() {
60 String::from(".")
61 } else {
62 format!(".{}.", package)
63 };
64
65 let rust_module =
68 if let Some(rust_root) = resolve_extern_prefix(package, effective_extern_paths) {
69 rust_root
70 } else {
71 package.replace('.', "::")
72 };
73
74 for msg in &file.message_type {
76 if let Some(name) = &msg.name {
77 let fqn = format!("{}{}", proto_prefix, name);
78 let rust_path = if rust_module.is_empty() {
79 name.clone()
80 } else {
81 format!("{}::{}", rust_module, name)
82 };
83 type_map.insert(fqn.clone(), rust_path);
84 package_of.insert(fqn.clone(), package.to_string());
85
86 let snake = to_snake_case(name);
88 let parent_mod = if rust_module.is_empty() {
89 snake
90 } else {
91 format!("{}::{}", rust_module, snake)
92 };
93 register_nested_types(
94 &mut type_map,
95 &mut package_of,
96 package,
97 &fqn,
98 &parent_mod,
99 msg,
100 );
101 register_nested_enum_closedness(
102 &mut enum_closedness,
103 &fqn,
104 &file_features,
105 msg,
106 );
107 }
108 }
109
110 for enum_type in &file.enum_type {
112 if let Some(name) = &enum_type.name {
113 let fqn = format!("{}{}", proto_prefix, name);
114 let rust_path = if rust_module.is_empty() {
115 name.clone()
116 } else {
117 format!("{}::{}", rust_module, name)
118 };
119 type_map.insert(fqn.clone(), rust_path);
120 package_of.insert(fqn.clone(), package.to_string());
121 register_enum_closedness(&mut enum_closedness, &fqn, &file_features, enum_type);
122 }
123 }
124 }
125
126 Self {
127 files,
128 config,
129 type_map,
130 package_of,
131 enum_closedness,
132 }
133 }
134
135 pub fn for_generate(
147 files: &'a [FileDescriptorProto],
148 files_to_generate: &[String],
149 config: &'a CodeGenConfig,
150 ) -> Self {
151 let paths = crate::effective_extern_paths(files, files_to_generate, config);
152 Self::new(files, config, &paths)
153 }
154
155 pub fn rust_type(&self, proto_fqn: &str) -> Option<&str> {
157 self.type_map.get(proto_fqn).map(|s| s.as_str())
158 }
159
160 pub fn is_enum_closed(&self, proto_fqn: &str) -> Option<bool> {
167 self.enum_closedness.get(proto_fqn).copied()
168 }
169
170 pub fn rust_type_relative(
185 &self,
186 proto_fqn: &str,
187 current_package: &str,
188 nesting: usize,
189 ) -> Option<String> {
190 let full_path = self.type_map.get(proto_fqn)?;
191
192 if full_path.starts_with("::") || full_path.starts_with("crate::") {
195 return Some(full_path.clone());
196 }
197
198 let target_package = self
199 .package_of
200 .get(proto_fqn)
201 .map(|s| s.as_str())
202 .unwrap_or("");
203
204 let target_rust_module = target_package.replace('.', "::");
207 let type_suffix = if target_rust_module.is_empty() {
208 full_path.as_str()
209 } else {
210 full_path
211 .strip_prefix(&format!("{}::", target_rust_module))
212 .unwrap_or(full_path)
213 };
214
215 if current_package == target_package {
216 if nesting == 0 {
218 return Some(type_suffix.to_string());
219 }
220 let supers = (0..nesting).map(|_| "super").collect::<Vec<_>>().join("::");
221 return Some(format!("{}::{}", supers, type_suffix));
222 }
223
224 let current_parts: Vec<&str> = if current_package.is_empty() {
226 vec![]
227 } else {
228 current_package.split('.').collect()
229 };
230 let target_parts: Vec<&str> = if target_package.is_empty() {
231 vec![]
232 } else {
233 target_package.split('.').collect()
234 };
235
236 let common_len = current_parts
238 .iter()
239 .zip(&target_parts)
240 .take_while(|(a, b)| a == b)
241 .count();
242
243 let up_count = (current_parts.len() - common_len) + nesting;
246
247 let down_parts = &target_parts[common_len..];
249
250 let mut segments: Vec<&str> = vec!["super"; up_count];
251 segments.extend_from_slice(down_parts);
252
253 let mut result = segments.join("::");
255 if !result.is_empty() {
256 result.push_str("::");
257 }
258 result.push_str(type_suffix);
259
260 Some(result)
261 }
262
263 pub fn use_bytes_type(&self, field_fqn: &str) -> bool {
270 self.config
271 .bytes_fields
272 .iter()
273 .any(|prefix| field_fqn.starts_with(prefix.as_str()))
274 }
275}
276
277fn resolve_extern_prefix(package: &str, extern_paths: &[(String, String)]) -> Option<String> {
284 let dotted = format!(".{}", package);
285
286 let mut best: Option<(&str, &str, usize)> = None;
289
290 for (proto_prefix, rust_prefix) in extern_paths {
291 if dotted == *proto_prefix {
292 return Some(rust_prefix.clone());
294 }
295 if let Some(rest) = dotted.strip_prefix(proto_prefix.as_str()) {
296 if rest.starts_with('.') {
297 let prefix_len = proto_prefix.len();
298 if best.is_none_or(|(_, _, best_len)| prefix_len > best_len) {
299 best = Some((proto_prefix, rust_prefix, prefix_len));
300 }
301 }
302 }
303 }
304
305 let (proto_prefix, rust_prefix, _) = best?;
306 let rest = dotted.strip_prefix(proto_prefix)?.strip_prefix('.')?;
307 let suffix = rest
308 .split('.')
309 .map(to_snake_case)
310 .collect::<Vec<_>>()
311 .join("::");
312 Some(format!("{}::{}", rust_prefix, suffix))
313}
314
315fn register_nested_types(
320 type_map: &mut HashMap<String, String>,
321 package_of: &mut HashMap<String, String>,
322 package: &str,
323 parent_fqn: &str,
324 parent_mod: &str,
325 msg: &crate::generated::descriptor::DescriptorProto,
326) {
327 for nested in &msg.nested_type {
328 if let Some(name) = &nested.name {
329 let fqn = format!("{}.{}", parent_fqn, name);
330 let rust_path = format!("{}::{}", parent_mod, name);
331 type_map.insert(fqn.clone(), rust_path);
332 package_of.insert(fqn.clone(), package.to_string());
333
334 let child_mod = format!("{}::{}", parent_mod, to_snake_case(name));
336 register_nested_types(type_map, package_of, package, &fqn, &child_mod, nested);
337 }
338 }
339
340 for enum_type in &msg.enum_type {
341 if let Some(name) = &enum_type.name {
342 let fqn = format!("{}.{}", parent_fqn, name);
343 let rust_path = format!("{}::{}", parent_mod, name);
344 type_map.insert(fqn.clone(), rust_path);
345 package_of.insert(fqn, package.to_string());
346 }
347 }
348}
349
350fn register_enum_closedness(
352 map: &mut HashMap<String, bool>,
353 fqn: &str,
354 parent_features: &ResolvedFeatures,
355 enum_desc: &EnumDescriptorProto,
356) {
357 let resolved = features::resolve_child(parent_features, features::enum_features(enum_desc));
358 let closed = resolved.enum_type == features::EnumType::Closed;
359 map.insert(fqn.to_string(), closed);
360}
361
362fn register_nested_enum_closedness(
365 map: &mut HashMap<String, bool>,
366 parent_fqn: &str,
367 parent_features: &ResolvedFeatures,
368 msg: &DescriptorProto,
369) {
370 let msg_features = features::resolve_child(parent_features, features::message_features(msg));
371 for enum_type in &msg.enum_type {
372 if let Some(name) = &enum_type.name {
373 let fqn = format!("{}.{}", parent_fqn, name);
374 register_enum_closedness(map, &fqn, &msg_features, enum_type);
375 }
376 }
377 for nested in &msg.nested_type {
378 if let Some(name) = &nested.name {
379 let fqn = format!("{}.{}", parent_fqn, name);
380 register_nested_enum_closedness(map, &fqn, &msg_features, nested);
381 }
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use crate::generated::descriptor::{DescriptorProto, EnumDescriptorProto, FileDescriptorProto};
389
390 fn make_file(
391 name: &str,
392 package: &str,
393 messages: Vec<DescriptorProto>,
394 enums: Vec<EnumDescriptorProto>,
395 ) -> FileDescriptorProto {
396 FileDescriptorProto {
397 name: Some(name.to_string()),
398 package: if package.is_empty() {
399 None
400 } else {
401 Some(package.to_string())
402 },
403 message_type: messages,
404 enum_type: enums,
405 ..Default::default()
406 }
407 }
408
409 fn msg(name: &str) -> DescriptorProto {
410 DescriptorProto {
411 name: Some(name.to_string()),
412 ..Default::default()
413 }
414 }
415
416 fn msg_with_nested(name: &str, nested: Vec<DescriptorProto>) -> DescriptorProto {
417 DescriptorProto {
418 name: Some(name.to_string()),
419 nested_type: nested,
420 ..Default::default()
421 }
422 }
423
424 fn msg_with_nested_and_enums(
425 name: &str,
426 nested: Vec<DescriptorProto>,
427 enums: Vec<EnumDescriptorProto>,
428 ) -> DescriptorProto {
429 DescriptorProto {
430 name: Some(name.to_string()),
431 nested_type: nested,
432 enum_type: enums,
433 ..Default::default()
434 }
435 }
436
437 fn enum_desc(name: &str) -> EnumDescriptorProto {
438 EnumDescriptorProto {
439 name: Some(name.to_string()),
440 ..Default::default()
441 }
442 }
443
444 fn enum_with_closed_feature(name: &str) -> EnumDescriptorProto {
445 use crate::generated::descriptor::{feature_set, EnumOptions, FeatureSet};
446 EnumDescriptorProto {
447 name: Some(name.to_string()),
448 options: buffa::MessageField::some(EnumOptions {
449 features: buffa::MessageField::some(FeatureSet {
450 enum_type: Some(feature_set::EnumType::CLOSED),
451 ..Default::default()
452 }),
453 ..Default::default()
454 }),
455 ..Default::default()
456 }
457 }
458
459 fn editions_file(
460 name: &str,
461 package: &str,
462 messages: Vec<DescriptorProto>,
463 enums: Vec<EnumDescriptorProto>,
464 ) -> FileDescriptorProto {
465 use crate::generated::descriptor::Edition;
466 FileDescriptorProto {
467 name: Some(name.to_string()),
468 package: Some(package.to_string()),
469 syntax: Some("editions".to_string()),
470 edition: Some(Edition::EDITION_2023),
471 message_type: messages,
472 enum_type: enums,
473 ..Default::default()
474 }
475 }
476
477 #[test]
480 fn test_message_with_package() {
481 let files = [make_file(
482 "test.proto",
483 "my.package",
484 vec![msg("Foo")],
485 vec![],
486 )];
487 let config = CodeGenConfig::default();
488 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
489 assert_eq!(ctx.rust_type(".my.package.Foo"), Some("my::package::Foo"));
490 }
491
492 #[test]
493 fn test_message_no_package() {
494 let files = [make_file("test.proto", "", vec![msg("Bar")], vec![])];
495 let config = CodeGenConfig::default();
496 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
497 assert_eq!(ctx.rust_type(".Bar"), Some("Bar"));
498 }
499
500 #[test]
501 fn test_nested_message_uses_module_path() {
502 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
503 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
504 let config = CodeGenConfig::default();
505 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
506 assert_eq!(ctx.rust_type(".pkg.Outer"), Some("pkg::Outer"));
507 assert_eq!(ctx.rust_type(".pkg.Outer.Inner"), Some("pkg::outer::Inner"));
509 }
510
511 #[test]
512 fn test_nested_message_no_package() {
513 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
514 let files = [make_file("test.proto", "", vec![outer], vec![])];
515 let config = CodeGenConfig::default();
516 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
517 assert_eq!(ctx.rust_type(".Outer"), Some("Outer"));
518 assert_eq!(ctx.rust_type(".Outer.Inner"), Some("outer::Inner"));
519 }
520
521 #[test]
522 fn test_deeply_nested_message() {
523 let deep = msg_with_nested("A", vec![msg_with_nested("B", vec![msg("C")])]);
524 let files = [make_file("test.proto", "pkg", vec![deep], vec![])];
525 let config = CodeGenConfig::default();
526 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
527 assert_eq!(ctx.rust_type(".pkg.A"), Some("pkg::A"));
528 assert_eq!(ctx.rust_type(".pkg.A.B"), Some("pkg::a::B"));
529 assert_eq!(ctx.rust_type(".pkg.A.B.C"), Some("pkg::a::b::C"));
530 }
531
532 #[test]
533 fn test_nested_enum_uses_module_path() {
534 let outer = msg_with_nested_and_enums("Outer", vec![], vec![enum_desc("Status")]);
535 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
536 let config = CodeGenConfig::default();
537 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
538 assert_eq!(
539 ctx.rust_type(".pkg.Outer.Status"),
540 Some("pkg::outer::Status")
541 );
542 }
543
544 #[test]
545 fn test_top_level_enum() {
546 let files = [make_file(
547 "test.proto",
548 "pkg",
549 vec![],
550 vec![enum_desc("Status")],
551 )];
552 let config = CodeGenConfig::default();
553 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
554 assert_eq!(ctx.rust_type(".pkg.Status"), Some("pkg::Status"));
555 }
556
557 #[test]
558 fn test_same_named_nested_types_in_different_parents_are_distinct() {
559 let outer1 = msg_with_nested("Outer1", vec![msg("Inner")]);
560 let outer2 = msg_with_nested("Outer2", vec![msg("Inner")]);
561 let files = [make_file("a.proto", "pkg", vec![outer1, outer2], vec![])];
562 let config = CodeGenConfig::default();
563 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
564 assert_eq!(
566 ctx.rust_type(".pkg.Outer1.Inner"),
567 Some("pkg::outer1::Inner")
568 );
569 assert_eq!(
570 ctx.rust_type(".pkg.Outer2.Inner"),
571 Some("pkg::outer2::Inner")
572 );
573 assert_ne!(
574 ctx.rust_type(".pkg.Outer1.Inner"),
575 ctx.rust_type(".pkg.Outer2.Inner")
576 );
577 }
578
579 #[test]
580 fn test_multiple_files() {
581 let files = [
582 make_file("a.proto", "ns.a", vec![msg("MsgA")], vec![]),
583 make_file("b.proto", "ns.b", vec![msg("MsgB")], vec![]),
584 ];
585 let config = CodeGenConfig::default();
586 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
587 assert_eq!(ctx.rust_type(".ns.a.MsgA"), Some("ns::a::MsgA"));
588 assert_eq!(ctx.rust_type(".ns.b.MsgB"), Some("ns::b::MsgB"));
589 }
590
591 #[test]
592 fn test_keyword_package_segment_in_type_map() {
593 let files = [make_file(
596 "latlng.proto",
597 "google.type",
598 vec![msg("LatLng")],
599 vec![],
600 )];
601 let config = CodeGenConfig::default();
602 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
603 assert_eq!(
604 ctx.rust_type(".google.type.LatLng"),
605 Some("google::type::LatLng")
606 );
607 }
608
609 #[test]
610 fn test_keyword_package_relative_same_package() {
611 let files = [make_file(
612 "latlng.proto",
613 "google.type",
614 vec![msg("LatLng"), msg("Expr")],
615 vec![],
616 )];
617 let config = CodeGenConfig::default();
618 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
619 assert_eq!(
621 ctx.rust_type_relative(".google.type.LatLng", "google.type", 0),
622 Some("LatLng".into())
623 );
624 }
625
626 #[test]
627 fn test_keyword_package_cross_package() {
628 let files = [
629 make_file("latlng.proto", "google.type", vec![msg("LatLng")], vec![]),
630 make_file("svc.proto", "google.cloud", vec![msg("Service")], vec![]),
631 ];
632 let config = CodeGenConfig::default();
633 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
634 assert_eq!(
637 ctx.rust_type_relative(".google.type.LatLng", "google.cloud", 0),
638 Some("super::type::LatLng".into())
639 );
640 }
641
642 #[test]
643 fn test_keyword_nested_message_module() {
644 let outer = msg_with_nested("Type", vec![msg("Inner")]);
646 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
647 let config = CodeGenConfig::default();
648 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
649 assert_eq!(ctx.rust_type(".pkg.Type"), Some("pkg::Type"));
650 assert_eq!(ctx.rust_type(".pkg.Type.Inner"), Some("pkg::type::Inner"));
651 }
652
653 #[test]
654 fn test_unknown_type_returns_none() {
655 let files = [make_file("test.proto", "pkg", vec![msg("Foo")], vec![])];
656 let config = CodeGenConfig::default();
657 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
658 assert_eq!(ctx.rust_type(".pkg.Unknown"), None);
659 }
660
661 #[test]
664 fn test_relative_same_package_top_level() {
665 let files = [make_file("a.proto", "pkg", vec![msg("Foo")], vec![])];
666 let config = CodeGenConfig::default();
667 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
668 assert_eq!(
670 ctx.rust_type_relative(".pkg.Foo", "pkg", 0),
671 Some("Foo".into())
672 );
673 }
674
675 #[test]
676 fn test_relative_cross_package() {
677 let files = [
678 make_file("a.proto", "pkg_a", vec![msg("Foo")], vec![]),
679 make_file("b.proto", "pkg_b", vec![msg("Bar")], vec![]),
680 ];
681 let config = CodeGenConfig::default();
682 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
683 assert_eq!(
685 ctx.rust_type_relative(".pkg_a.Foo", "pkg_b", 0),
686 Some("super::pkg_a::Foo".into())
687 );
688 }
689
690 #[test]
691 fn test_relative_no_package() {
692 let files = [make_file("a.proto", "", vec![msg("Foo")], vec![])];
693 let config = CodeGenConfig::default();
694 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
695 assert_eq!(ctx.rust_type_relative(".Foo", "", 0), Some("Foo".into()));
696 }
697
698 #[test]
699 fn test_relative_unknown_returns_none() {
700 let files = [make_file("a.proto", "pkg", vec![msg("Foo")], vec![])];
701 let config = CodeGenConfig::default();
702 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
703 assert_eq!(ctx.rust_type_relative(".pkg.Unknown", "pkg", 0), None);
704 }
705
706 #[test]
707 fn test_relative_dotted_package() {
708 let files = [make_file("a.proto", "my.pkg", vec![msg("Foo")], vec![])];
709 let config = CodeGenConfig::default();
710 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
711 assert_eq!(
712 ctx.rust_type_relative(".my.pkg.Foo", "my.pkg", 0),
713 Some("Foo".into())
714 );
715 }
716
717 #[test]
718 fn test_relative_cross_dotted_packages() {
719 let files = [
720 make_file(
721 "timestamp.proto",
722 "google.protobuf",
723 vec![msg("Timestamp")],
724 vec![],
725 ),
726 make_file(
727 "test.proto",
728 "protobuf_test_messages.proto3",
729 vec![msg("TestAllTypesProto3")],
730 vec![],
731 ),
732 ];
733 let config = CodeGenConfig::default();
734 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
735
736 assert_eq!(
738 ctx.rust_type_relative(
739 ".google.protobuf.Timestamp",
740 "protobuf_test_messages.proto3",
741 0,
742 ),
743 Some("super::super::google::protobuf::Timestamp".into())
744 );
745 }
746
747 #[test]
748 fn test_relative_nested_type_from_same_package() {
749 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
751 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
752 let config = CodeGenConfig::default();
753 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
754
755 assert_eq!(
757 ctx.rust_type_relative(".pkg.Outer.Inner", "pkg", 0),
758 Some("outer::Inner".into())
759 );
760 }
761
762 #[test]
763 fn test_relative_shared_prefix_not_confused() {
764 let files = [
765 make_file("ab.proto", "a.b", vec![msg("Msg1")], vec![]),
766 make_file("abc.proto", "a.bc", vec![msg("Msg2")], vec![]),
767 ];
768 let config = CodeGenConfig::default();
769 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
770
771 assert_eq!(
773 ctx.rust_type_relative(".a.b.Msg1", "a.bc", 0),
774 Some("super::b::Msg1".into())
775 );
776 assert_eq!(
778 ctx.rust_type_relative(".a.bc.Msg2", "a.b", 0),
779 Some("super::bc::Msg2".into())
780 );
781 }
782
783 #[test]
786 fn test_resolve_extern_prefix_exact_match() {
787 let result = resolve_extern_prefix(
788 "my.common",
789 &[(".my.common".into(), "::common_protos".into())],
790 );
791 assert_eq!(result, Some("::common_protos".into()));
792 }
793
794 #[test]
795 fn test_resolve_extern_prefix_sub_package() {
796 let result = resolve_extern_prefix(
797 "my.common.sub",
798 &[(".my.common".into(), "::common_protos".into())],
799 );
800 assert_eq!(result, Some("::common_protos::sub".into()));
801 }
802
803 #[test]
804 fn test_resolve_extern_prefix_no_match() {
805 let result = resolve_extern_prefix(
806 "other.pkg",
807 &[(".my.common".into(), "::common_protos".into())],
808 );
809 assert_eq!(result, None);
810 }
811
812 #[test]
813 fn test_resolve_extern_prefix_partial_name_no_match() {
814 let result = resolve_extern_prefix(
816 "my.commonext",
817 &[(".my.common".into(), "::common_protos".into())],
818 );
819 assert_eq!(result, None);
820 }
821
822 #[test]
823 fn test_resolve_extern_prefix_longest_match_wins() {
824 let result = resolve_extern_prefix(
826 "my.common.sub",
827 &[
828 (".my".into(), "::crate_a".into()),
829 (".my.common".into(), "::crate_b".into()),
830 ],
831 );
832 assert_eq!(result, Some("::crate_b::sub".into()));
833 }
834
835 #[test]
836 fn test_extern_path_top_level_message() {
837 let files = [make_file(
838 "common.proto",
839 "my.common",
840 vec![msg("SharedMsg")],
841 vec![],
842 )];
843 let config = CodeGenConfig {
844 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
845 ..Default::default()
846 };
847 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
848 assert_eq!(
849 ctx.rust_type(".my.common.SharedMsg"),
850 Some("::common_protos::SharedMsg")
851 );
852 }
853
854 #[test]
855 fn test_extern_path_nested_message() {
856 let files = [make_file(
857 "common.proto",
858 "my.common",
859 vec![msg_with_nested("Outer", vec![msg("Inner")])],
860 vec![],
861 )];
862 let config = CodeGenConfig {
863 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
864 ..Default::default()
865 };
866 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
867 assert_eq!(
868 ctx.rust_type(".my.common.Outer"),
869 Some("::common_protos::Outer")
870 );
871 assert_eq!(
872 ctx.rust_type(".my.common.Outer.Inner"),
873 Some("::common_protos::outer::Inner")
874 );
875 }
876
877 #[test]
878 fn test_extern_path_enum() {
879 let files = [make_file(
880 "common.proto",
881 "my.common",
882 vec![],
883 vec![enum_desc("Status")],
884 )];
885 let config = CodeGenConfig {
886 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
887 ..Default::default()
888 };
889 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
890 assert_eq!(
891 ctx.rust_type(".my.common.Status"),
892 Some("::common_protos::Status")
893 );
894 }
895
896 #[test]
897 fn test_extern_path_does_not_affect_other_packages() {
898 let files = [
899 make_file("common.proto", "my.common", vec![msg("SharedMsg")], vec![]),
900 make_file(
901 "service.proto",
902 "my.service",
903 vec![msg("MyService")],
904 vec![],
905 ),
906 ];
907 let config = CodeGenConfig {
908 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
909 ..Default::default()
910 };
911 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
912 assert_eq!(
914 ctx.rust_type(".my.common.SharedMsg"),
915 Some("::common_protos::SharedMsg")
916 );
917 assert_eq!(
919 ctx.rust_type(".my.service.MyService"),
920 Some("my::service::MyService")
921 );
922 }
923
924 #[test]
925 fn test_extern_path_relative_returns_absolute() {
926 let files = [
929 make_file("common.proto", "my.common", vec![msg("SharedMsg")], vec![]),
930 make_file(
931 "service.proto",
932 "my.service",
933 vec![msg("MyService")],
934 vec![],
935 ),
936 ];
937 let config = CodeGenConfig {
938 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
939 ..Default::default()
940 };
941 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
942 assert_eq!(
944 ctx.rust_type_relative(".my.common.SharedMsg", "my.service", 0),
945 Some("::common_protos::SharedMsg".into())
946 );
947 }
948
949 #[test]
952 fn test_is_enum_closed_proto3_default_open() {
953 let files = [make_file("a.proto", "p", vec![], vec![enum_desc("E")])];
954 let config = CodeGenConfig::default();
955 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
956 assert_eq!(ctx.is_enum_closed(".p.E"), Some(true));
960 }
961
962 #[test]
963 fn test_is_enum_closed_editions_default_open() {
964 let files = [editions_file("a.proto", "p", vec![], vec![enum_desc("E")])];
965 let config = CodeGenConfig::default();
966 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
967 assert_eq!(ctx.is_enum_closed(".p.E"), Some(false));
969 }
970
971 #[test]
972 fn test_is_enum_closed_per_enum_override() {
973 let files = [editions_file(
976 "a.proto",
977 "p",
978 vec![],
979 vec![enum_desc("Open"), enum_with_closed_feature("Closed")],
980 )];
981 let config = CodeGenConfig::default();
982 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
983 assert_eq!(ctx.is_enum_closed(".p.Open"), Some(false));
984 assert_eq!(ctx.is_enum_closed(".p.Closed"), Some(true));
985 }
986
987 #[test]
988 fn test_is_enum_closed_nested_per_enum_override() {
989 let files = [editions_file(
991 "a.proto",
992 "p",
993 vec![msg_with_nested_and_enums(
994 "M",
995 vec![],
996 vec![enum_with_closed_feature("Inner")],
997 )],
998 vec![],
999 )];
1000 let config = CodeGenConfig::default();
1001 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1002 assert_eq!(ctx.is_enum_closed(".p.M.Inner"), Some(true));
1003 }
1004
1005 #[test]
1006 fn test_is_enum_closed_unknown_enum_returns_none() {
1007 let files = [editions_file("a.proto", "p", vec![], vec![])];
1008 let config = CodeGenConfig::default();
1009 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1010 assert_eq!(ctx.is_enum_closed(".other.Unknown"), None);
1012 }
1013
1014 #[test]
1015 fn test_for_generate_auto_injects_wkt_mapping() {
1016 let ts_msg = DescriptorProto {
1019 name: Some("Timestamp".into()),
1020 ..Default::default()
1021 };
1022 let files = [FileDescriptorProto {
1023 name: Some("google/protobuf/timestamp.proto".into()),
1024 package: Some("google.protobuf".into()),
1025 syntax: Some("proto3".into()),
1026 message_type: vec![ts_msg],
1027 ..Default::default()
1028 }];
1029 let config = CodeGenConfig::default();
1030 let ctx = CodeGenContext::for_generate(&files, &["other.proto".into()], &config);
1032 assert_eq!(
1033 ctx.rust_type(".google.protobuf.Timestamp"),
1034 Some("::buffa_types::google::protobuf::Timestamp"),
1035 "WKT auto-mapping must be applied via for_generate"
1036 );
1037 }
1038
1039 #[test]
1040 fn test_for_generate_suppresses_wkt_when_generating_wkt() {
1041 let ts_msg = DescriptorProto {
1044 name: Some("Timestamp".into()),
1045 ..Default::default()
1046 };
1047 let files = [FileDescriptorProto {
1048 name: Some("google/protobuf/timestamp.proto".into()),
1049 package: Some("google.protobuf".into()),
1050 syntax: Some("proto3".into()),
1051 message_type: vec![ts_msg],
1052 ..Default::default()
1053 }];
1054 let config = CodeGenConfig::default();
1055 let ctx = CodeGenContext::for_generate(
1056 &files,
1057 &["google/protobuf/timestamp.proto".into()],
1058 &config,
1059 );
1060 assert_eq!(
1062 ctx.rust_type(".google.protobuf.Timestamp"),
1063 Some("google::protobuf::Timestamp")
1064 );
1065 }
1066}