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 comment_map: HashMap<String, String>,
52}
53
54impl<'a> CodeGenContext<'a> {
55 pub fn new(
61 files: &'a [FileDescriptorProto],
62 config: &'a CodeGenConfig,
63 effective_extern_paths: &[(String, String)],
64 ) -> Self {
65 let mut type_map = HashMap::new();
66 let mut package_of = HashMap::new();
67 let mut enum_closedness = HashMap::new();
68 let mut comment_map = HashMap::new();
69
70 for file in files {
71 comment_map.extend(crate::comments::fqn_comments(file));
72 let package = file.package.as_deref().unwrap_or("");
73 let file_features = features::for_file(file);
74 let proto_prefix = if package.is_empty() {
75 String::from(".")
76 } else {
77 format!(".{}.", package)
78 };
79
80 let rust_module =
83 if let Some(rust_root) = resolve_extern_prefix(package, effective_extern_paths) {
84 rust_root
85 } else {
86 package.replace('.', "::")
87 };
88
89 for msg in &file.message_type {
91 if let Some(name) = &msg.name {
92 let fqn = format!("{}{}", proto_prefix, name);
93 let rust_path = if rust_module.is_empty() {
94 name.clone()
95 } else {
96 format!("{}::{}", rust_module, name)
97 };
98 type_map.insert(fqn.clone(), rust_path);
99 package_of.insert(fqn.clone(), package.to_string());
100
101 let snake = to_snake_case(name);
103 let parent_mod = if rust_module.is_empty() {
104 snake
105 } else {
106 format!("{}::{}", rust_module, snake)
107 };
108 register_nested_types(
109 &mut type_map,
110 &mut package_of,
111 package,
112 &fqn,
113 &parent_mod,
114 msg,
115 );
116 register_nested_enum_closedness(
117 &mut enum_closedness,
118 &fqn,
119 &file_features,
120 msg,
121 );
122 }
123 }
124
125 for enum_type in &file.enum_type {
127 if let Some(name) = &enum_type.name {
128 let fqn = format!("{}{}", proto_prefix, name);
129 let rust_path = if rust_module.is_empty() {
130 name.clone()
131 } else {
132 format!("{}::{}", rust_module, name)
133 };
134 type_map.insert(fqn.clone(), rust_path);
135 package_of.insert(fqn.clone(), package.to_string());
136 register_enum_closedness(&mut enum_closedness, &fqn, &file_features, enum_type);
137 }
138 }
139 }
140
141 Self {
142 files,
143 config,
144 type_map,
145 package_of,
146 enum_closedness,
147 comment_map,
148 }
149 }
150
151 pub fn for_generate(
163 files: &'a [FileDescriptorProto],
164 files_to_generate: &[String],
165 config: &'a CodeGenConfig,
166 ) -> Self {
167 let paths = crate::effective_extern_paths(files, files_to_generate, config);
168 Self::new(files, config, &paths)
169 }
170
171 pub fn rust_type(&self, proto_fqn: &str) -> Option<&str> {
173 self.type_map.get(proto_fqn).map(|s| s.as_str())
174 }
175
176 pub fn comment(&self, fqn: &str) -> Option<&str> {
185 self.comment_map.get(fqn).map(|s| s.as_str())
186 }
187
188 pub fn is_enum_closed(&self, proto_fqn: &str) -> Option<bool> {
195 self.enum_closedness.get(proto_fqn).copied()
196 }
197
198 pub fn rust_type_relative(
213 &self,
214 proto_fqn: &str,
215 current_package: &str,
216 nesting: usize,
217 ) -> Option<String> {
218 let full_path = self.type_map.get(proto_fqn)?;
219
220 if full_path.starts_with("::") || full_path.starts_with("crate::") {
223 return Some(full_path.clone());
224 }
225
226 let target_package = self
227 .package_of
228 .get(proto_fqn)
229 .map(|s| s.as_str())
230 .unwrap_or("");
231
232 let target_rust_module = target_package.replace('.', "::");
235 let type_suffix = if target_rust_module.is_empty() {
236 full_path.as_str()
237 } else {
238 full_path
239 .strip_prefix(&format!("{}::", target_rust_module))
240 .unwrap_or(full_path)
241 };
242
243 if current_package == target_package {
244 if nesting == 0 {
246 return Some(type_suffix.to_string());
247 }
248 let supers = (0..nesting).map(|_| "super").collect::<Vec<_>>().join("::");
249 return Some(format!("{}::{}", supers, type_suffix));
250 }
251
252 let current_parts: Vec<&str> = if current_package.is_empty() {
254 vec![]
255 } else {
256 current_package.split('.').collect()
257 };
258 let target_parts: Vec<&str> = if target_package.is_empty() {
259 vec![]
260 } else {
261 target_package.split('.').collect()
262 };
263
264 let common_len = current_parts
266 .iter()
267 .zip(&target_parts)
268 .take_while(|(a, b)| a == b)
269 .count();
270
271 let up_count = (current_parts.len() - common_len) + nesting;
274
275 let down_parts = &target_parts[common_len..];
277
278 let mut segments: Vec<&str> = vec!["super"; up_count];
279 segments.extend_from_slice(down_parts);
280
281 let mut result = segments.join("::");
283 if !result.is_empty() {
284 result.push_str("::");
285 }
286 result.push_str(type_suffix);
287
288 Some(result)
289 }
290
291 pub fn use_bytes_type(&self, field_fqn: &str) -> bool {
298 self.config
299 .bytes_fields
300 .iter()
301 .any(|prefix| field_fqn.starts_with(prefix.as_str()))
302 }
303}
304
305fn resolve_extern_prefix(package: &str, extern_paths: &[(String, String)]) -> Option<String> {
312 let dotted = format!(".{}", package);
313
314 let mut best: Option<(&str, &str, usize)> = None;
317
318 for (proto_prefix, rust_prefix) in extern_paths {
319 if dotted == *proto_prefix {
320 return Some(rust_prefix.clone());
322 }
323 if let Some(rest) = dotted.strip_prefix(proto_prefix.as_str()) {
324 if proto_prefix == "." || rest.starts_with('.') {
326 let prefix_len = proto_prefix.len();
327 if best.is_none_or(|(_, _, best_len)| prefix_len > best_len) {
328 best = Some((proto_prefix, rust_prefix, prefix_len));
329 }
330 }
331 }
332 }
333
334 let (proto_prefix, rust_prefix, _) = best?;
335 let rest = dotted.strip_prefix(proto_prefix)?;
336 let rest = rest.strip_prefix('.').unwrap_or(rest);
337 let suffix = rest
338 .split('.')
339 .map(to_snake_case)
340 .collect::<Vec<_>>()
341 .join("::");
342 Some(format!("{}::{}", rust_prefix, suffix))
343}
344
345fn register_nested_types(
350 type_map: &mut HashMap<String, String>,
351 package_of: &mut HashMap<String, String>,
352 package: &str,
353 parent_fqn: &str,
354 parent_mod: &str,
355 msg: &crate::generated::descriptor::DescriptorProto,
356) {
357 for nested in &msg.nested_type {
358 if let Some(name) = &nested.name {
359 let fqn = format!("{}.{}", parent_fqn, name);
360 let rust_path = format!("{}::{}", parent_mod, name);
361 type_map.insert(fqn.clone(), rust_path);
362 package_of.insert(fqn.clone(), package.to_string());
363
364 let child_mod = format!("{}::{}", parent_mod, to_snake_case(name));
366 register_nested_types(type_map, package_of, package, &fqn, &child_mod, nested);
367 }
368 }
369
370 for enum_type in &msg.enum_type {
371 if let Some(name) = &enum_type.name {
372 let fqn = format!("{}.{}", parent_fqn, name);
373 let rust_path = format!("{}::{}", parent_mod, name);
374 type_map.insert(fqn.clone(), rust_path);
375 package_of.insert(fqn, package.to_string());
376 }
377 }
378}
379
380fn register_enum_closedness(
382 map: &mut HashMap<String, bool>,
383 fqn: &str,
384 parent_features: &ResolvedFeatures,
385 enum_desc: &EnumDescriptorProto,
386) {
387 let resolved = features::resolve_child(parent_features, features::enum_features(enum_desc));
388 let closed = resolved.enum_type == features::EnumType::Closed;
389 map.insert(fqn.to_string(), closed);
390}
391
392fn register_nested_enum_closedness(
395 map: &mut HashMap<String, bool>,
396 parent_fqn: &str,
397 parent_features: &ResolvedFeatures,
398 msg: &DescriptorProto,
399) {
400 let msg_features = features::resolve_child(parent_features, features::message_features(msg));
401 for enum_type in &msg.enum_type {
402 if let Some(name) = &enum_type.name {
403 let fqn = format!("{}.{}", parent_fqn, name);
404 register_enum_closedness(map, &fqn, &msg_features, enum_type);
405 }
406 }
407 for nested in &msg.nested_type {
408 if let Some(name) = &nested.name {
409 let fqn = format!("{}.{}", parent_fqn, name);
410 register_nested_enum_closedness(map, &fqn, &msg_features, nested);
411 }
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418 use crate::generated::descriptor::{DescriptorProto, EnumDescriptorProto, FileDescriptorProto};
419
420 fn make_file(
421 name: &str,
422 package: &str,
423 messages: Vec<DescriptorProto>,
424 enums: Vec<EnumDescriptorProto>,
425 ) -> FileDescriptorProto {
426 FileDescriptorProto {
427 name: Some(name.to_string()),
428 package: if package.is_empty() {
429 None
430 } else {
431 Some(package.to_string())
432 },
433 message_type: messages,
434 enum_type: enums,
435 ..Default::default()
436 }
437 }
438
439 fn msg(name: &str) -> DescriptorProto {
440 DescriptorProto {
441 name: Some(name.to_string()),
442 ..Default::default()
443 }
444 }
445
446 fn msg_with_nested(name: &str, nested: Vec<DescriptorProto>) -> DescriptorProto {
447 DescriptorProto {
448 name: Some(name.to_string()),
449 nested_type: nested,
450 ..Default::default()
451 }
452 }
453
454 fn msg_with_nested_and_enums(
455 name: &str,
456 nested: Vec<DescriptorProto>,
457 enums: Vec<EnumDescriptorProto>,
458 ) -> DescriptorProto {
459 DescriptorProto {
460 name: Some(name.to_string()),
461 nested_type: nested,
462 enum_type: enums,
463 ..Default::default()
464 }
465 }
466
467 fn enum_desc(name: &str) -> EnumDescriptorProto {
468 EnumDescriptorProto {
469 name: Some(name.to_string()),
470 ..Default::default()
471 }
472 }
473
474 fn enum_with_closed_feature(name: &str) -> EnumDescriptorProto {
475 use crate::generated::descriptor::{feature_set, EnumOptions, FeatureSet};
476 EnumDescriptorProto {
477 name: Some(name.to_string()),
478 options: buffa::MessageField::some(EnumOptions {
479 features: buffa::MessageField::some(FeatureSet {
480 enum_type: Some(feature_set::EnumType::CLOSED),
481 ..Default::default()
482 }),
483 ..Default::default()
484 }),
485 ..Default::default()
486 }
487 }
488
489 fn editions_file(
490 name: &str,
491 package: &str,
492 messages: Vec<DescriptorProto>,
493 enums: Vec<EnumDescriptorProto>,
494 ) -> FileDescriptorProto {
495 use crate::generated::descriptor::Edition;
496 FileDescriptorProto {
497 name: Some(name.to_string()),
498 package: Some(package.to_string()),
499 syntax: Some("editions".to_string()),
500 edition: Some(Edition::EDITION_2023),
501 message_type: messages,
502 enum_type: enums,
503 ..Default::default()
504 }
505 }
506
507 #[test]
510 fn test_message_with_package() {
511 let files = [make_file(
512 "test.proto",
513 "my.package",
514 vec![msg("Foo")],
515 vec![],
516 )];
517 let config = CodeGenConfig::default();
518 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
519 assert_eq!(ctx.rust_type(".my.package.Foo"), Some("my::package::Foo"));
520 }
521
522 #[test]
523 fn test_message_no_package() {
524 let files = [make_file("test.proto", "", vec![msg("Bar")], vec![])];
525 let config = CodeGenConfig::default();
526 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
527 assert_eq!(ctx.rust_type(".Bar"), Some("Bar"));
528 }
529
530 #[test]
531 fn test_nested_message_uses_module_path() {
532 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
533 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
534 let config = CodeGenConfig::default();
535 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
536 assert_eq!(ctx.rust_type(".pkg.Outer"), Some("pkg::Outer"));
537 assert_eq!(ctx.rust_type(".pkg.Outer.Inner"), Some("pkg::outer::Inner"));
539 }
540
541 #[test]
542 fn test_nested_message_no_package() {
543 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
544 let files = [make_file("test.proto", "", vec![outer], vec![])];
545 let config = CodeGenConfig::default();
546 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
547 assert_eq!(ctx.rust_type(".Outer"), Some("Outer"));
548 assert_eq!(ctx.rust_type(".Outer.Inner"), Some("outer::Inner"));
549 }
550
551 #[test]
552 fn test_deeply_nested_message() {
553 let deep = msg_with_nested("A", vec![msg_with_nested("B", vec![msg("C")])]);
554 let files = [make_file("test.proto", "pkg", vec![deep], vec![])];
555 let config = CodeGenConfig::default();
556 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
557 assert_eq!(ctx.rust_type(".pkg.A"), Some("pkg::A"));
558 assert_eq!(ctx.rust_type(".pkg.A.B"), Some("pkg::a::B"));
559 assert_eq!(ctx.rust_type(".pkg.A.B.C"), Some("pkg::a::b::C"));
560 }
561
562 #[test]
563 fn test_nested_enum_uses_module_path() {
564 let outer = msg_with_nested_and_enums("Outer", vec![], vec![enum_desc("Status")]);
565 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
566 let config = CodeGenConfig::default();
567 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
568 assert_eq!(
569 ctx.rust_type(".pkg.Outer.Status"),
570 Some("pkg::outer::Status")
571 );
572 }
573
574 #[test]
575 fn test_top_level_enum() {
576 let files = [make_file(
577 "test.proto",
578 "pkg",
579 vec![],
580 vec![enum_desc("Status")],
581 )];
582 let config = CodeGenConfig::default();
583 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
584 assert_eq!(ctx.rust_type(".pkg.Status"), Some("pkg::Status"));
585 }
586
587 #[test]
588 fn test_same_named_nested_types_in_different_parents_are_distinct() {
589 let outer1 = msg_with_nested("Outer1", vec![msg("Inner")]);
590 let outer2 = msg_with_nested("Outer2", vec![msg("Inner")]);
591 let files = [make_file("a.proto", "pkg", vec![outer1, outer2], vec![])];
592 let config = CodeGenConfig::default();
593 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
594 assert_eq!(
596 ctx.rust_type(".pkg.Outer1.Inner"),
597 Some("pkg::outer1::Inner")
598 );
599 assert_eq!(
600 ctx.rust_type(".pkg.Outer2.Inner"),
601 Some("pkg::outer2::Inner")
602 );
603 assert_ne!(
604 ctx.rust_type(".pkg.Outer1.Inner"),
605 ctx.rust_type(".pkg.Outer2.Inner")
606 );
607 }
608
609 #[test]
610 fn test_multiple_files() {
611 let files = [
612 make_file("a.proto", "ns.a", vec![msg("MsgA")], vec![]),
613 make_file("b.proto", "ns.b", vec![msg("MsgB")], vec![]),
614 ];
615 let config = CodeGenConfig::default();
616 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
617 assert_eq!(ctx.rust_type(".ns.a.MsgA"), Some("ns::a::MsgA"));
618 assert_eq!(ctx.rust_type(".ns.b.MsgB"), Some("ns::b::MsgB"));
619 }
620
621 #[test]
622 fn test_keyword_package_segment_in_type_map() {
623 let files = [make_file(
626 "latlng.proto",
627 "google.type",
628 vec![msg("LatLng")],
629 vec![],
630 )];
631 let config = CodeGenConfig::default();
632 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
633 assert_eq!(
634 ctx.rust_type(".google.type.LatLng"),
635 Some("google::type::LatLng")
636 );
637 }
638
639 #[test]
640 fn test_keyword_package_relative_same_package() {
641 let files = [make_file(
642 "latlng.proto",
643 "google.type",
644 vec![msg("LatLng"), msg("Expr")],
645 vec![],
646 )];
647 let config = CodeGenConfig::default();
648 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
649 assert_eq!(
651 ctx.rust_type_relative(".google.type.LatLng", "google.type", 0),
652 Some("LatLng".into())
653 );
654 }
655
656 #[test]
657 fn test_keyword_package_cross_package() {
658 let files = [
659 make_file("latlng.proto", "google.type", vec![msg("LatLng")], vec![]),
660 make_file("svc.proto", "google.cloud", vec![msg("Service")], vec![]),
661 ];
662 let config = CodeGenConfig::default();
663 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
664 assert_eq!(
667 ctx.rust_type_relative(".google.type.LatLng", "google.cloud", 0),
668 Some("super::type::LatLng".into())
669 );
670 }
671
672 #[test]
673 fn test_keyword_nested_message_module() {
674 let outer = msg_with_nested("Type", vec![msg("Inner")]);
676 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
677 let config = CodeGenConfig::default();
678 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
679 assert_eq!(ctx.rust_type(".pkg.Type"), Some("pkg::Type"));
680 assert_eq!(ctx.rust_type(".pkg.Type.Inner"), Some("pkg::type::Inner"));
681 }
682
683 #[test]
684 fn test_unknown_type_returns_none() {
685 let files = [make_file("test.proto", "pkg", vec![msg("Foo")], vec![])];
686 let config = CodeGenConfig::default();
687 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
688 assert_eq!(ctx.rust_type(".pkg.Unknown"), None);
689 }
690
691 #[test]
694 fn test_relative_same_package_top_level() {
695 let files = [make_file("a.proto", "pkg", vec![msg("Foo")], vec![])];
696 let config = CodeGenConfig::default();
697 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
698 assert_eq!(
700 ctx.rust_type_relative(".pkg.Foo", "pkg", 0),
701 Some("Foo".into())
702 );
703 }
704
705 #[test]
706 fn test_relative_cross_package() {
707 let files = [
708 make_file("a.proto", "pkg_a", vec![msg("Foo")], vec![]),
709 make_file("b.proto", "pkg_b", vec![msg("Bar")], vec![]),
710 ];
711 let config = CodeGenConfig::default();
712 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
713 assert_eq!(
715 ctx.rust_type_relative(".pkg_a.Foo", "pkg_b", 0),
716 Some("super::pkg_a::Foo".into())
717 );
718 }
719
720 #[test]
721 fn test_relative_no_package() {
722 let files = [make_file("a.proto", "", vec![msg("Foo")], vec![])];
723 let config = CodeGenConfig::default();
724 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
725 assert_eq!(ctx.rust_type_relative(".Foo", "", 0), Some("Foo".into()));
726 }
727
728 #[test]
729 fn test_relative_unknown_returns_none() {
730 let files = [make_file("a.proto", "pkg", vec![msg("Foo")], vec![])];
731 let config = CodeGenConfig::default();
732 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
733 assert_eq!(ctx.rust_type_relative(".pkg.Unknown", "pkg", 0), None);
734 }
735
736 #[test]
737 fn test_relative_dotted_package() {
738 let files = [make_file("a.proto", "my.pkg", vec![msg("Foo")], vec![])];
739 let config = CodeGenConfig::default();
740 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
741 assert_eq!(
742 ctx.rust_type_relative(".my.pkg.Foo", "my.pkg", 0),
743 Some("Foo".into())
744 );
745 }
746
747 #[test]
748 fn test_relative_cross_dotted_packages() {
749 let files = [
750 make_file(
751 "timestamp.proto",
752 "google.protobuf",
753 vec![msg("Timestamp")],
754 vec![],
755 ),
756 make_file(
757 "test.proto",
758 "protobuf_test_messages.proto3",
759 vec![msg("TestAllTypesProto3")],
760 vec![],
761 ),
762 ];
763 let config = CodeGenConfig::default();
764 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
765
766 assert_eq!(
768 ctx.rust_type_relative(
769 ".google.protobuf.Timestamp",
770 "protobuf_test_messages.proto3",
771 0,
772 ),
773 Some("super::super::google::protobuf::Timestamp".into())
774 );
775 }
776
777 #[test]
778 fn test_relative_nested_type_from_same_package() {
779 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
781 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
782 let config = CodeGenConfig::default();
783 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
784
785 assert_eq!(
787 ctx.rust_type_relative(".pkg.Outer.Inner", "pkg", 0),
788 Some("outer::Inner".into())
789 );
790 }
791
792 #[test]
793 fn test_relative_shared_prefix_not_confused() {
794 let files = [
795 make_file("ab.proto", "a.b", vec![msg("Msg1")], vec![]),
796 make_file("abc.proto", "a.bc", vec![msg("Msg2")], vec![]),
797 ];
798 let config = CodeGenConfig::default();
799 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
800
801 assert_eq!(
803 ctx.rust_type_relative(".a.b.Msg1", "a.bc", 0),
804 Some("super::b::Msg1".into())
805 );
806 assert_eq!(
808 ctx.rust_type_relative(".a.bc.Msg2", "a.b", 0),
809 Some("super::bc::Msg2".into())
810 );
811 }
812
813 #[test]
816 fn test_resolve_extern_prefix_exact_match() {
817 let result = resolve_extern_prefix(
818 "my.common",
819 &[(".my.common".into(), "::common_protos".into())],
820 );
821 assert_eq!(result, Some("::common_protos".into()));
822 }
823
824 #[test]
825 fn test_resolve_extern_prefix_sub_package() {
826 let result = resolve_extern_prefix(
827 "my.common.sub",
828 &[(".my.common".into(), "::common_protos".into())],
829 );
830 assert_eq!(result, Some("::common_protos::sub".into()));
831 }
832
833 #[test]
834 fn test_resolve_extern_prefix_no_match() {
835 let result = resolve_extern_prefix(
836 "other.pkg",
837 &[(".my.common".into(), "::common_protos".into())],
838 );
839 assert_eq!(result, None);
840 }
841
842 #[test]
843 fn test_resolve_extern_prefix_partial_name_no_match() {
844 let result = resolve_extern_prefix(
846 "my.commonext",
847 &[(".my.common".into(), "::common_protos".into())],
848 );
849 assert_eq!(result, None);
850 }
851
852 #[test]
853 fn test_resolve_extern_prefix_longest_match_wins() {
854 let result = resolve_extern_prefix(
856 "my.common.sub",
857 &[
858 (".my".into(), "::crate_a".into()),
859 (".my.common".into(), "::crate_b".into()),
860 ],
861 );
862 assert_eq!(result, Some("::crate_b::sub".into()));
863 }
864
865 #[test]
866 fn test_resolve_extern_prefix_catchall() {
867 let result = resolve_extern_prefix("greet.v1", &[(".".into(), "crate::proto".into())]);
868 assert_eq!(result, Some("crate::proto::greet::v1".into()));
869 }
870
871 #[test]
872 fn test_resolve_extern_prefix_catchall_empty_pkg() {
873 let result = resolve_extern_prefix("", &[(".".into(), "crate::proto".into())]);
876 assert_eq!(result, Some("crate::proto".into()));
877 }
878
879 #[test]
880 fn test_resolve_extern_prefix_catchall_longest_wins() {
881 let result = resolve_extern_prefix(
884 "google.protobuf",
885 &[
886 (".".into(), "crate::proto".into()),
887 (
888 ".google.protobuf".into(),
889 "::buffa_types::google::protobuf".into(),
890 ),
891 ],
892 );
893 assert_eq!(result, Some("::buffa_types::google::protobuf".into()));
894 }
895
896 #[test]
897 fn test_resolve_extern_prefix_catchall_keyword_package() {
898 let result = resolve_extern_prefix("google.type", &[(".".into(), "crate::proto".into())]);
901 assert_eq!(result, Some("crate::proto::google::type".into()));
902 }
903
904 #[test]
905 fn test_extern_path_top_level_message() {
906 let files = [make_file(
907 "common.proto",
908 "my.common",
909 vec![msg("SharedMsg")],
910 vec![],
911 )];
912 let config = CodeGenConfig {
913 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
914 ..Default::default()
915 };
916 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
917 assert_eq!(
918 ctx.rust_type(".my.common.SharedMsg"),
919 Some("::common_protos::SharedMsg")
920 );
921 }
922
923 #[test]
924 fn test_extern_path_nested_message() {
925 let files = [make_file(
926 "common.proto",
927 "my.common",
928 vec![msg_with_nested("Outer", vec![msg("Inner")])],
929 vec![],
930 )];
931 let config = CodeGenConfig {
932 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
933 ..Default::default()
934 };
935 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
936 assert_eq!(
937 ctx.rust_type(".my.common.Outer"),
938 Some("::common_protos::Outer")
939 );
940 assert_eq!(
941 ctx.rust_type(".my.common.Outer.Inner"),
942 Some("::common_protos::outer::Inner")
943 );
944 }
945
946 #[test]
947 fn test_extern_path_enum() {
948 let files = [make_file(
949 "common.proto",
950 "my.common",
951 vec![],
952 vec![enum_desc("Status")],
953 )];
954 let config = CodeGenConfig {
955 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
956 ..Default::default()
957 };
958 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
959 assert_eq!(
960 ctx.rust_type(".my.common.Status"),
961 Some("::common_protos::Status")
962 );
963 }
964
965 #[test]
966 fn test_extern_path_does_not_affect_other_packages() {
967 let files = [
968 make_file("common.proto", "my.common", vec![msg("SharedMsg")], vec![]),
969 make_file(
970 "service.proto",
971 "my.service",
972 vec![msg("MyService")],
973 vec![],
974 ),
975 ];
976 let config = CodeGenConfig {
977 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
978 ..Default::default()
979 };
980 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
981 assert_eq!(
983 ctx.rust_type(".my.common.SharedMsg"),
984 Some("::common_protos::SharedMsg")
985 );
986 assert_eq!(
988 ctx.rust_type(".my.service.MyService"),
989 Some("my::service::MyService")
990 );
991 }
992
993 #[test]
994 fn test_extern_path_relative_returns_absolute() {
995 let files = [
998 make_file("common.proto", "my.common", vec![msg("SharedMsg")], vec![]),
999 make_file(
1000 "service.proto",
1001 "my.service",
1002 vec![msg("MyService")],
1003 vec![],
1004 ),
1005 ];
1006 let config = CodeGenConfig {
1007 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
1008 ..Default::default()
1009 };
1010 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1011 assert_eq!(
1013 ctx.rust_type_relative(".my.common.SharedMsg", "my.service", 0),
1014 Some("::common_protos::SharedMsg".into())
1015 );
1016 }
1017
1018 #[test]
1021 fn test_is_enum_closed_proto3_default_open() {
1022 let files = [make_file("a.proto", "p", vec![], vec![enum_desc("E")])];
1023 let config = CodeGenConfig::default();
1024 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1025 assert_eq!(ctx.is_enum_closed(".p.E"), Some(true));
1029 }
1030
1031 #[test]
1032 fn test_is_enum_closed_editions_default_open() {
1033 let files = [editions_file("a.proto", "p", vec![], vec![enum_desc("E")])];
1034 let config = CodeGenConfig::default();
1035 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1036 assert_eq!(ctx.is_enum_closed(".p.E"), Some(false));
1038 }
1039
1040 #[test]
1041 fn test_is_enum_closed_per_enum_override() {
1042 let files = [editions_file(
1045 "a.proto",
1046 "p",
1047 vec![],
1048 vec![enum_desc("Open"), enum_with_closed_feature("Closed")],
1049 )];
1050 let config = CodeGenConfig::default();
1051 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1052 assert_eq!(ctx.is_enum_closed(".p.Open"), Some(false));
1053 assert_eq!(ctx.is_enum_closed(".p.Closed"), Some(true));
1054 }
1055
1056 #[test]
1057 fn test_is_enum_closed_nested_per_enum_override() {
1058 let files = [editions_file(
1060 "a.proto",
1061 "p",
1062 vec![msg_with_nested_and_enums(
1063 "M",
1064 vec![],
1065 vec![enum_with_closed_feature("Inner")],
1066 )],
1067 vec![],
1068 )];
1069 let config = CodeGenConfig::default();
1070 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1071 assert_eq!(ctx.is_enum_closed(".p.M.Inner"), Some(true));
1072 }
1073
1074 #[test]
1075 fn test_is_enum_closed_unknown_enum_returns_none() {
1076 let files = [editions_file("a.proto", "p", vec![], vec![])];
1077 let config = CodeGenConfig::default();
1078 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1079 assert_eq!(ctx.is_enum_closed(".other.Unknown"), None);
1081 }
1082
1083 #[test]
1084 fn test_for_generate_auto_injects_wkt_mapping() {
1085 let ts_msg = DescriptorProto {
1088 name: Some("Timestamp".into()),
1089 ..Default::default()
1090 };
1091 let files = [FileDescriptorProto {
1092 name: Some("google/protobuf/timestamp.proto".into()),
1093 package: Some("google.protobuf".into()),
1094 syntax: Some("proto3".into()),
1095 message_type: vec![ts_msg],
1096 ..Default::default()
1097 }];
1098 let config = CodeGenConfig::default();
1099 let ctx = CodeGenContext::for_generate(&files, &["other.proto".into()], &config);
1101 assert_eq!(
1102 ctx.rust_type(".google.protobuf.Timestamp"),
1103 Some("::buffa_types::google::protobuf::Timestamp"),
1104 "WKT auto-mapping must be applied via for_generate"
1105 );
1106 }
1107
1108 #[test]
1109 fn test_for_generate_suppresses_wkt_when_generating_wkt() {
1110 let ts_msg = DescriptorProto {
1113 name: Some("Timestamp".into()),
1114 ..Default::default()
1115 };
1116 let files = [FileDescriptorProto {
1117 name: Some("google/protobuf/timestamp.proto".into()),
1118 package: Some("google.protobuf".into()),
1119 syntax: Some("proto3".into()),
1120 message_type: vec![ts_msg],
1121 ..Default::default()
1122 }];
1123 let config = CodeGenConfig::default();
1124 let ctx = CodeGenContext::for_generate(
1125 &files,
1126 &["google/protobuf/timestamp.proto".into()],
1127 &config,
1128 );
1129 assert_eq!(
1131 ctx.rust_type(".google.protobuf.Timestamp"),
1132 Some("google::protobuf::Timestamp")
1133 );
1134 }
1135}