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 proto_prefix == "." || rest.starts_with('.') {
298 let prefix_len = proto_prefix.len();
299 if best.is_none_or(|(_, _, best_len)| prefix_len > best_len) {
300 best = Some((proto_prefix, rust_prefix, prefix_len));
301 }
302 }
303 }
304 }
305
306 let (proto_prefix, rust_prefix, _) = best?;
307 let rest = dotted.strip_prefix(proto_prefix)?;
308 let rest = rest.strip_prefix('.').unwrap_or(rest);
309 let suffix = rest
310 .split('.')
311 .map(to_snake_case)
312 .collect::<Vec<_>>()
313 .join("::");
314 Some(format!("{}::{}", rust_prefix, suffix))
315}
316
317fn register_nested_types(
322 type_map: &mut HashMap<String, String>,
323 package_of: &mut HashMap<String, String>,
324 package: &str,
325 parent_fqn: &str,
326 parent_mod: &str,
327 msg: &crate::generated::descriptor::DescriptorProto,
328) {
329 for nested in &msg.nested_type {
330 if let Some(name) = &nested.name {
331 let fqn = format!("{}.{}", parent_fqn, name);
332 let rust_path = format!("{}::{}", parent_mod, name);
333 type_map.insert(fqn.clone(), rust_path);
334 package_of.insert(fqn.clone(), package.to_string());
335
336 let child_mod = format!("{}::{}", parent_mod, to_snake_case(name));
338 register_nested_types(type_map, package_of, package, &fqn, &child_mod, nested);
339 }
340 }
341
342 for enum_type in &msg.enum_type {
343 if let Some(name) = &enum_type.name {
344 let fqn = format!("{}.{}", parent_fqn, name);
345 let rust_path = format!("{}::{}", parent_mod, name);
346 type_map.insert(fqn.clone(), rust_path);
347 package_of.insert(fqn, package.to_string());
348 }
349 }
350}
351
352fn register_enum_closedness(
354 map: &mut HashMap<String, bool>,
355 fqn: &str,
356 parent_features: &ResolvedFeatures,
357 enum_desc: &EnumDescriptorProto,
358) {
359 let resolved = features::resolve_child(parent_features, features::enum_features(enum_desc));
360 let closed = resolved.enum_type == features::EnumType::Closed;
361 map.insert(fqn.to_string(), closed);
362}
363
364fn register_nested_enum_closedness(
367 map: &mut HashMap<String, bool>,
368 parent_fqn: &str,
369 parent_features: &ResolvedFeatures,
370 msg: &DescriptorProto,
371) {
372 let msg_features = features::resolve_child(parent_features, features::message_features(msg));
373 for enum_type in &msg.enum_type {
374 if let Some(name) = &enum_type.name {
375 let fqn = format!("{}.{}", parent_fqn, name);
376 register_enum_closedness(map, &fqn, &msg_features, enum_type);
377 }
378 }
379 for nested in &msg.nested_type {
380 if let Some(name) = &nested.name {
381 let fqn = format!("{}.{}", parent_fqn, name);
382 register_nested_enum_closedness(map, &fqn, &msg_features, nested);
383 }
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390 use crate::generated::descriptor::{DescriptorProto, EnumDescriptorProto, FileDescriptorProto};
391
392 fn make_file(
393 name: &str,
394 package: &str,
395 messages: Vec<DescriptorProto>,
396 enums: Vec<EnumDescriptorProto>,
397 ) -> FileDescriptorProto {
398 FileDescriptorProto {
399 name: Some(name.to_string()),
400 package: if package.is_empty() {
401 None
402 } else {
403 Some(package.to_string())
404 },
405 message_type: messages,
406 enum_type: enums,
407 ..Default::default()
408 }
409 }
410
411 fn msg(name: &str) -> DescriptorProto {
412 DescriptorProto {
413 name: Some(name.to_string()),
414 ..Default::default()
415 }
416 }
417
418 fn msg_with_nested(name: &str, nested: Vec<DescriptorProto>) -> DescriptorProto {
419 DescriptorProto {
420 name: Some(name.to_string()),
421 nested_type: nested,
422 ..Default::default()
423 }
424 }
425
426 fn msg_with_nested_and_enums(
427 name: &str,
428 nested: Vec<DescriptorProto>,
429 enums: Vec<EnumDescriptorProto>,
430 ) -> DescriptorProto {
431 DescriptorProto {
432 name: Some(name.to_string()),
433 nested_type: nested,
434 enum_type: enums,
435 ..Default::default()
436 }
437 }
438
439 fn enum_desc(name: &str) -> EnumDescriptorProto {
440 EnumDescriptorProto {
441 name: Some(name.to_string()),
442 ..Default::default()
443 }
444 }
445
446 fn enum_with_closed_feature(name: &str) -> EnumDescriptorProto {
447 use crate::generated::descriptor::{feature_set, EnumOptions, FeatureSet};
448 EnumDescriptorProto {
449 name: Some(name.to_string()),
450 options: buffa::MessageField::some(EnumOptions {
451 features: buffa::MessageField::some(FeatureSet {
452 enum_type: Some(feature_set::EnumType::CLOSED),
453 ..Default::default()
454 }),
455 ..Default::default()
456 }),
457 ..Default::default()
458 }
459 }
460
461 fn editions_file(
462 name: &str,
463 package: &str,
464 messages: Vec<DescriptorProto>,
465 enums: Vec<EnumDescriptorProto>,
466 ) -> FileDescriptorProto {
467 use crate::generated::descriptor::Edition;
468 FileDescriptorProto {
469 name: Some(name.to_string()),
470 package: Some(package.to_string()),
471 syntax: Some("editions".to_string()),
472 edition: Some(Edition::EDITION_2023),
473 message_type: messages,
474 enum_type: enums,
475 ..Default::default()
476 }
477 }
478
479 #[test]
482 fn test_message_with_package() {
483 let files = [make_file(
484 "test.proto",
485 "my.package",
486 vec![msg("Foo")],
487 vec![],
488 )];
489 let config = CodeGenConfig::default();
490 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
491 assert_eq!(ctx.rust_type(".my.package.Foo"), Some("my::package::Foo"));
492 }
493
494 #[test]
495 fn test_message_no_package() {
496 let files = [make_file("test.proto", "", vec![msg("Bar")], vec![])];
497 let config = CodeGenConfig::default();
498 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
499 assert_eq!(ctx.rust_type(".Bar"), Some("Bar"));
500 }
501
502 #[test]
503 fn test_nested_message_uses_module_path() {
504 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
505 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
506 let config = CodeGenConfig::default();
507 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
508 assert_eq!(ctx.rust_type(".pkg.Outer"), Some("pkg::Outer"));
509 assert_eq!(ctx.rust_type(".pkg.Outer.Inner"), Some("pkg::outer::Inner"));
511 }
512
513 #[test]
514 fn test_nested_message_no_package() {
515 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
516 let files = [make_file("test.proto", "", vec![outer], vec![])];
517 let config = CodeGenConfig::default();
518 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
519 assert_eq!(ctx.rust_type(".Outer"), Some("Outer"));
520 assert_eq!(ctx.rust_type(".Outer.Inner"), Some("outer::Inner"));
521 }
522
523 #[test]
524 fn test_deeply_nested_message() {
525 let deep = msg_with_nested("A", vec![msg_with_nested("B", vec![msg("C")])]);
526 let files = [make_file("test.proto", "pkg", vec![deep], vec![])];
527 let config = CodeGenConfig::default();
528 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
529 assert_eq!(ctx.rust_type(".pkg.A"), Some("pkg::A"));
530 assert_eq!(ctx.rust_type(".pkg.A.B"), Some("pkg::a::B"));
531 assert_eq!(ctx.rust_type(".pkg.A.B.C"), Some("pkg::a::b::C"));
532 }
533
534 #[test]
535 fn test_nested_enum_uses_module_path() {
536 let outer = msg_with_nested_and_enums("Outer", vec![], vec![enum_desc("Status")]);
537 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
538 let config = CodeGenConfig::default();
539 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
540 assert_eq!(
541 ctx.rust_type(".pkg.Outer.Status"),
542 Some("pkg::outer::Status")
543 );
544 }
545
546 #[test]
547 fn test_top_level_enum() {
548 let files = [make_file(
549 "test.proto",
550 "pkg",
551 vec![],
552 vec![enum_desc("Status")],
553 )];
554 let config = CodeGenConfig::default();
555 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
556 assert_eq!(ctx.rust_type(".pkg.Status"), Some("pkg::Status"));
557 }
558
559 #[test]
560 fn test_same_named_nested_types_in_different_parents_are_distinct() {
561 let outer1 = msg_with_nested("Outer1", vec![msg("Inner")]);
562 let outer2 = msg_with_nested("Outer2", vec![msg("Inner")]);
563 let files = [make_file("a.proto", "pkg", vec![outer1, outer2], vec![])];
564 let config = CodeGenConfig::default();
565 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
566 assert_eq!(
568 ctx.rust_type(".pkg.Outer1.Inner"),
569 Some("pkg::outer1::Inner")
570 );
571 assert_eq!(
572 ctx.rust_type(".pkg.Outer2.Inner"),
573 Some("pkg::outer2::Inner")
574 );
575 assert_ne!(
576 ctx.rust_type(".pkg.Outer1.Inner"),
577 ctx.rust_type(".pkg.Outer2.Inner")
578 );
579 }
580
581 #[test]
582 fn test_multiple_files() {
583 let files = [
584 make_file("a.proto", "ns.a", vec![msg("MsgA")], vec![]),
585 make_file("b.proto", "ns.b", vec![msg("MsgB")], vec![]),
586 ];
587 let config = CodeGenConfig::default();
588 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
589 assert_eq!(ctx.rust_type(".ns.a.MsgA"), Some("ns::a::MsgA"));
590 assert_eq!(ctx.rust_type(".ns.b.MsgB"), Some("ns::b::MsgB"));
591 }
592
593 #[test]
594 fn test_keyword_package_segment_in_type_map() {
595 let files = [make_file(
598 "latlng.proto",
599 "google.type",
600 vec![msg("LatLng")],
601 vec![],
602 )];
603 let config = CodeGenConfig::default();
604 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
605 assert_eq!(
606 ctx.rust_type(".google.type.LatLng"),
607 Some("google::type::LatLng")
608 );
609 }
610
611 #[test]
612 fn test_keyword_package_relative_same_package() {
613 let files = [make_file(
614 "latlng.proto",
615 "google.type",
616 vec![msg("LatLng"), msg("Expr")],
617 vec![],
618 )];
619 let config = CodeGenConfig::default();
620 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
621 assert_eq!(
623 ctx.rust_type_relative(".google.type.LatLng", "google.type", 0),
624 Some("LatLng".into())
625 );
626 }
627
628 #[test]
629 fn test_keyword_package_cross_package() {
630 let files = [
631 make_file("latlng.proto", "google.type", vec![msg("LatLng")], vec![]),
632 make_file("svc.proto", "google.cloud", vec![msg("Service")], vec![]),
633 ];
634 let config = CodeGenConfig::default();
635 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
636 assert_eq!(
639 ctx.rust_type_relative(".google.type.LatLng", "google.cloud", 0),
640 Some("super::type::LatLng".into())
641 );
642 }
643
644 #[test]
645 fn test_keyword_nested_message_module() {
646 let outer = msg_with_nested("Type", vec![msg("Inner")]);
648 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
649 let config = CodeGenConfig::default();
650 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
651 assert_eq!(ctx.rust_type(".pkg.Type"), Some("pkg::Type"));
652 assert_eq!(ctx.rust_type(".pkg.Type.Inner"), Some("pkg::type::Inner"));
653 }
654
655 #[test]
656 fn test_unknown_type_returns_none() {
657 let files = [make_file("test.proto", "pkg", vec![msg("Foo")], vec![])];
658 let config = CodeGenConfig::default();
659 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
660 assert_eq!(ctx.rust_type(".pkg.Unknown"), None);
661 }
662
663 #[test]
666 fn test_relative_same_package_top_level() {
667 let files = [make_file("a.proto", "pkg", vec![msg("Foo")], vec![])];
668 let config = CodeGenConfig::default();
669 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
670 assert_eq!(
672 ctx.rust_type_relative(".pkg.Foo", "pkg", 0),
673 Some("Foo".into())
674 );
675 }
676
677 #[test]
678 fn test_relative_cross_package() {
679 let files = [
680 make_file("a.proto", "pkg_a", vec![msg("Foo")], vec![]),
681 make_file("b.proto", "pkg_b", vec![msg("Bar")], vec![]),
682 ];
683 let config = CodeGenConfig::default();
684 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
685 assert_eq!(
687 ctx.rust_type_relative(".pkg_a.Foo", "pkg_b", 0),
688 Some("super::pkg_a::Foo".into())
689 );
690 }
691
692 #[test]
693 fn test_relative_no_package() {
694 let files = [make_file("a.proto", "", vec![msg("Foo")], vec![])];
695 let config = CodeGenConfig::default();
696 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
697 assert_eq!(ctx.rust_type_relative(".Foo", "", 0), Some("Foo".into()));
698 }
699
700 #[test]
701 fn test_relative_unknown_returns_none() {
702 let files = [make_file("a.proto", "pkg", vec![msg("Foo")], vec![])];
703 let config = CodeGenConfig::default();
704 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
705 assert_eq!(ctx.rust_type_relative(".pkg.Unknown", "pkg", 0), None);
706 }
707
708 #[test]
709 fn test_relative_dotted_package() {
710 let files = [make_file("a.proto", "my.pkg", vec![msg("Foo")], vec![])];
711 let config = CodeGenConfig::default();
712 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
713 assert_eq!(
714 ctx.rust_type_relative(".my.pkg.Foo", "my.pkg", 0),
715 Some("Foo".into())
716 );
717 }
718
719 #[test]
720 fn test_relative_cross_dotted_packages() {
721 let files = [
722 make_file(
723 "timestamp.proto",
724 "google.protobuf",
725 vec![msg("Timestamp")],
726 vec![],
727 ),
728 make_file(
729 "test.proto",
730 "protobuf_test_messages.proto3",
731 vec![msg("TestAllTypesProto3")],
732 vec![],
733 ),
734 ];
735 let config = CodeGenConfig::default();
736 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
737
738 assert_eq!(
740 ctx.rust_type_relative(
741 ".google.protobuf.Timestamp",
742 "protobuf_test_messages.proto3",
743 0,
744 ),
745 Some("super::super::google::protobuf::Timestamp".into())
746 );
747 }
748
749 #[test]
750 fn test_relative_nested_type_from_same_package() {
751 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
753 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
754 let config = CodeGenConfig::default();
755 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
756
757 assert_eq!(
759 ctx.rust_type_relative(".pkg.Outer.Inner", "pkg", 0),
760 Some("outer::Inner".into())
761 );
762 }
763
764 #[test]
765 fn test_relative_shared_prefix_not_confused() {
766 let files = [
767 make_file("ab.proto", "a.b", vec![msg("Msg1")], vec![]),
768 make_file("abc.proto", "a.bc", vec![msg("Msg2")], vec![]),
769 ];
770 let config = CodeGenConfig::default();
771 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
772
773 assert_eq!(
775 ctx.rust_type_relative(".a.b.Msg1", "a.bc", 0),
776 Some("super::b::Msg1".into())
777 );
778 assert_eq!(
780 ctx.rust_type_relative(".a.bc.Msg2", "a.b", 0),
781 Some("super::bc::Msg2".into())
782 );
783 }
784
785 #[test]
788 fn test_resolve_extern_prefix_exact_match() {
789 let result = resolve_extern_prefix(
790 "my.common",
791 &[(".my.common".into(), "::common_protos".into())],
792 );
793 assert_eq!(result, Some("::common_protos".into()));
794 }
795
796 #[test]
797 fn test_resolve_extern_prefix_sub_package() {
798 let result = resolve_extern_prefix(
799 "my.common.sub",
800 &[(".my.common".into(), "::common_protos".into())],
801 );
802 assert_eq!(result, Some("::common_protos::sub".into()));
803 }
804
805 #[test]
806 fn test_resolve_extern_prefix_no_match() {
807 let result = resolve_extern_prefix(
808 "other.pkg",
809 &[(".my.common".into(), "::common_protos".into())],
810 );
811 assert_eq!(result, None);
812 }
813
814 #[test]
815 fn test_resolve_extern_prefix_partial_name_no_match() {
816 let result = resolve_extern_prefix(
818 "my.commonext",
819 &[(".my.common".into(), "::common_protos".into())],
820 );
821 assert_eq!(result, None);
822 }
823
824 #[test]
825 fn test_resolve_extern_prefix_longest_match_wins() {
826 let result = resolve_extern_prefix(
828 "my.common.sub",
829 &[
830 (".my".into(), "::crate_a".into()),
831 (".my.common".into(), "::crate_b".into()),
832 ],
833 );
834 assert_eq!(result, Some("::crate_b::sub".into()));
835 }
836
837 #[test]
838 fn test_resolve_extern_prefix_catchall() {
839 let result = resolve_extern_prefix("greet.v1", &[(".".into(), "crate::proto".into())]);
840 assert_eq!(result, Some("crate::proto::greet::v1".into()));
841 }
842
843 #[test]
844 fn test_resolve_extern_prefix_catchall_empty_pkg() {
845 let result = resolve_extern_prefix("", &[(".".into(), "crate::proto".into())]);
848 assert_eq!(result, Some("crate::proto".into()));
849 }
850
851 #[test]
852 fn test_resolve_extern_prefix_catchall_longest_wins() {
853 let result = resolve_extern_prefix(
856 "google.protobuf",
857 &[
858 (".".into(), "crate::proto".into()),
859 (
860 ".google.protobuf".into(),
861 "::buffa_types::google::protobuf".into(),
862 ),
863 ],
864 );
865 assert_eq!(result, Some("::buffa_types::google::protobuf".into()));
866 }
867
868 #[test]
869 fn test_resolve_extern_prefix_catchall_keyword_package() {
870 let result = resolve_extern_prefix("google.type", &[(".".into(), "crate::proto".into())]);
873 assert_eq!(result, Some("crate::proto::google::type".into()));
874 }
875
876 #[test]
877 fn test_extern_path_top_level_message() {
878 let files = [make_file(
879 "common.proto",
880 "my.common",
881 vec![msg("SharedMsg")],
882 vec![],
883 )];
884 let config = CodeGenConfig {
885 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
886 ..Default::default()
887 };
888 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
889 assert_eq!(
890 ctx.rust_type(".my.common.SharedMsg"),
891 Some("::common_protos::SharedMsg")
892 );
893 }
894
895 #[test]
896 fn test_extern_path_nested_message() {
897 let files = [make_file(
898 "common.proto",
899 "my.common",
900 vec![msg_with_nested("Outer", vec![msg("Inner")])],
901 vec![],
902 )];
903 let config = CodeGenConfig {
904 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
905 ..Default::default()
906 };
907 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
908 assert_eq!(
909 ctx.rust_type(".my.common.Outer"),
910 Some("::common_protos::Outer")
911 );
912 assert_eq!(
913 ctx.rust_type(".my.common.Outer.Inner"),
914 Some("::common_protos::outer::Inner")
915 );
916 }
917
918 #[test]
919 fn test_extern_path_enum() {
920 let files = [make_file(
921 "common.proto",
922 "my.common",
923 vec![],
924 vec![enum_desc("Status")],
925 )];
926 let config = CodeGenConfig {
927 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
928 ..Default::default()
929 };
930 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
931 assert_eq!(
932 ctx.rust_type(".my.common.Status"),
933 Some("::common_protos::Status")
934 );
935 }
936
937 #[test]
938 fn test_extern_path_does_not_affect_other_packages() {
939 let files = [
940 make_file("common.proto", "my.common", vec![msg("SharedMsg")], vec![]),
941 make_file(
942 "service.proto",
943 "my.service",
944 vec![msg("MyService")],
945 vec![],
946 ),
947 ];
948 let config = CodeGenConfig {
949 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
950 ..Default::default()
951 };
952 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
953 assert_eq!(
955 ctx.rust_type(".my.common.SharedMsg"),
956 Some("::common_protos::SharedMsg")
957 );
958 assert_eq!(
960 ctx.rust_type(".my.service.MyService"),
961 Some("my::service::MyService")
962 );
963 }
964
965 #[test]
966 fn test_extern_path_relative_returns_absolute() {
967 let files = [
970 make_file("common.proto", "my.common", vec![msg("SharedMsg")], vec![]),
971 make_file(
972 "service.proto",
973 "my.service",
974 vec![msg("MyService")],
975 vec![],
976 ),
977 ];
978 let config = CodeGenConfig {
979 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
980 ..Default::default()
981 };
982 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
983 assert_eq!(
985 ctx.rust_type_relative(".my.common.SharedMsg", "my.service", 0),
986 Some("::common_protos::SharedMsg".into())
987 );
988 }
989
990 #[test]
993 fn test_is_enum_closed_proto3_default_open() {
994 let files = [make_file("a.proto", "p", vec![], vec![enum_desc("E")])];
995 let config = CodeGenConfig::default();
996 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
997 assert_eq!(ctx.is_enum_closed(".p.E"), Some(true));
1001 }
1002
1003 #[test]
1004 fn test_is_enum_closed_editions_default_open() {
1005 let files = [editions_file("a.proto", "p", vec![], vec![enum_desc("E")])];
1006 let config = CodeGenConfig::default();
1007 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1008 assert_eq!(ctx.is_enum_closed(".p.E"), Some(false));
1010 }
1011
1012 #[test]
1013 fn test_is_enum_closed_per_enum_override() {
1014 let files = [editions_file(
1017 "a.proto",
1018 "p",
1019 vec![],
1020 vec![enum_desc("Open"), enum_with_closed_feature("Closed")],
1021 )];
1022 let config = CodeGenConfig::default();
1023 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1024 assert_eq!(ctx.is_enum_closed(".p.Open"), Some(false));
1025 assert_eq!(ctx.is_enum_closed(".p.Closed"), Some(true));
1026 }
1027
1028 #[test]
1029 fn test_is_enum_closed_nested_per_enum_override() {
1030 let files = [editions_file(
1032 "a.proto",
1033 "p",
1034 vec![msg_with_nested_and_enums(
1035 "M",
1036 vec![],
1037 vec![enum_with_closed_feature("Inner")],
1038 )],
1039 vec![],
1040 )];
1041 let config = CodeGenConfig::default();
1042 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1043 assert_eq!(ctx.is_enum_closed(".p.M.Inner"), Some(true));
1044 }
1045
1046 #[test]
1047 fn test_is_enum_closed_unknown_enum_returns_none() {
1048 let files = [editions_file("a.proto", "p", vec![], vec![])];
1049 let config = CodeGenConfig::default();
1050 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1051 assert_eq!(ctx.is_enum_closed(".other.Unknown"), None);
1053 }
1054
1055 #[test]
1056 fn test_for_generate_auto_injects_wkt_mapping() {
1057 let ts_msg = DescriptorProto {
1060 name: Some("Timestamp".into()),
1061 ..Default::default()
1062 };
1063 let files = [FileDescriptorProto {
1064 name: Some("google/protobuf/timestamp.proto".into()),
1065 package: Some("google.protobuf".into()),
1066 syntax: Some("proto3".into()),
1067 message_type: vec![ts_msg],
1068 ..Default::default()
1069 }];
1070 let config = CodeGenConfig::default();
1071 let ctx = CodeGenContext::for_generate(&files, &["other.proto".into()], &config);
1073 assert_eq!(
1074 ctx.rust_type(".google.protobuf.Timestamp"),
1075 Some("::buffa_types::google::protobuf::Timestamp"),
1076 "WKT auto-mapping must be applied via for_generate"
1077 );
1078 }
1079
1080 #[test]
1081 fn test_for_generate_suppresses_wkt_when_generating_wkt() {
1082 let ts_msg = DescriptorProto {
1085 name: Some("Timestamp".into()),
1086 ..Default::default()
1087 };
1088 let files = [FileDescriptorProto {
1089 name: Some("google/protobuf/timestamp.proto".into()),
1090 package: Some("google.protobuf".into()),
1091 syntax: Some("proto3".into()),
1092 message_type: vec![ts_msg],
1093 ..Default::default()
1094 }];
1095 let config = CodeGenConfig::default();
1096 let ctx = CodeGenContext::for_generate(
1097 &files,
1098 &["google/protobuf/timestamp.proto".into()],
1099 &config,
1100 );
1101 assert_eq!(
1103 ctx.rust_type(".google.protobuf.Timestamp"),
1104 Some("google::protobuf::Timestamp")
1105 );
1106 }
1107}