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 const SENTINEL_MOD: &str = "__buffa";
17
18#[derive(Debug, Clone)]
24pub struct SplitPath {
25 pub to_package: String,
33 pub within_package: String,
36 pub is_extern: bool,
39}
40
41pub struct CodeGenContext<'a> {
47 pub files: &'a [FileDescriptorProto],
49 pub config: &'a CodeGenConfig,
51 pub type_map: HashMap<String, String>,
57 package_of: HashMap<String, String>,
62 enum_closedness: HashMap<String, bool>,
70 comment_map: HashMap<String, String>,
83}
84
85impl<'a> CodeGenContext<'a> {
86 pub fn new(
92 files: &'a [FileDescriptorProto],
93 config: &'a CodeGenConfig,
94 effective_extern_paths: &[(String, String)],
95 ) -> Self {
96 let mut type_map = HashMap::new();
97 let mut package_of = HashMap::new();
98 let mut enum_closedness = HashMap::new();
99 let mut comment_map = HashMap::new();
100
101 for file in files {
102 comment_map.extend(crate::comments::fqn_comments(file));
103 let package = file.package.as_deref().unwrap_or("");
104 let file_features = features::for_file(file);
105 let proto_prefix = if package.is_empty() {
106 String::from(".")
107 } else {
108 format!(".{}.", package)
109 };
110
111 let rust_module =
114 if let Some(rust_root) = resolve_extern_prefix(package, effective_extern_paths) {
115 rust_root
116 } else {
117 package.replace('.', "::")
118 };
119
120 for msg in &file.message_type {
122 if let Some(name) = &msg.name {
123 let fqn = format!("{}{}", proto_prefix, name);
124 let rust_path = if rust_module.is_empty() {
125 name.clone()
126 } else {
127 format!("{}::{}", rust_module, name)
128 };
129 type_map.insert(fqn.clone(), rust_path);
130 package_of.insert(fqn.clone(), package.to_string());
131
132 let snake = to_snake_case(name);
134 let parent_mod = if rust_module.is_empty() {
135 snake
136 } else {
137 format!("{}::{}", rust_module, snake)
138 };
139 register_nested_types(
140 &mut type_map,
141 &mut package_of,
142 package,
143 &fqn,
144 &parent_mod,
145 msg,
146 );
147 register_nested_enum_closedness(
148 &mut enum_closedness,
149 &fqn,
150 &file_features,
151 msg,
152 );
153 }
154 }
155
156 for enum_type in &file.enum_type {
158 if let Some(name) = &enum_type.name {
159 let fqn = format!("{}{}", proto_prefix, name);
160 let rust_path = if rust_module.is_empty() {
161 name.clone()
162 } else {
163 format!("{}::{}", rust_module, name)
164 };
165 type_map.insert(fqn.clone(), rust_path);
166 package_of.insert(fqn.clone(), package.to_string());
167 register_enum_closedness(&mut enum_closedness, &fqn, &file_features, enum_type);
168 }
169 }
170 }
171
172 Self {
173 files,
174 config,
175 type_map,
176 package_of,
177 enum_closedness,
178 comment_map,
179 }
180 }
181
182 pub fn for_generate(
194 files: &'a [FileDescriptorProto],
195 files_to_generate: &[String],
196 config: &'a CodeGenConfig,
197 ) -> Self {
198 let paths = crate::effective_extern_paths(files, files_to_generate, config);
199 Self::new(files, config, &paths)
200 }
201
202 pub fn rust_type(&self, proto_fqn: &str) -> Option<&str> {
204 self.type_map.get(proto_fqn).map(|s| s.as_str())
205 }
206
207 pub fn comment(&self, fqn: &str) -> Option<&str> {
216 self.comment_map.get(fqn).map(|s| s.as_str())
217 }
218
219 pub fn is_enum_closed(&self, proto_fqn: &str) -> Option<bool> {
226 self.enum_closedness.get(proto_fqn).copied()
227 }
228
229 pub fn rust_type_relative(
244 &self,
245 proto_fqn: &str,
246 current_package: &str,
247 nesting: usize,
248 ) -> Option<String> {
249 let full_path = self.type_map.get(proto_fqn)?;
250
251 if full_path.starts_with("::") || full_path.starts_with("crate::") {
254 return Some(full_path.clone());
255 }
256
257 let target_package = self
258 .package_of
259 .get(proto_fqn)
260 .map(|s| s.as_str())
261 .unwrap_or("");
262
263 let target_rust_module = target_package.replace('.', "::");
266 let type_suffix = if target_rust_module.is_empty() {
267 full_path.as_str()
268 } else {
269 full_path
270 .strip_prefix(&format!("{}::", target_rust_module))
271 .unwrap_or(full_path)
272 };
273
274 if current_package == target_package {
275 if nesting == 0 {
277 return Some(type_suffix.to_string());
278 }
279 let supers = (0..nesting).map(|_| "super").collect::<Vec<_>>().join("::");
280 return Some(format!("{}::{}", supers, type_suffix));
281 }
282
283 let current_parts: Vec<&str> = if current_package.is_empty() {
285 vec![]
286 } else {
287 current_package.split('.').collect()
288 };
289 let target_parts: Vec<&str> = if target_package.is_empty() {
290 vec![]
291 } else {
292 target_package.split('.').collect()
293 };
294
295 let common_len = current_parts
297 .iter()
298 .zip(&target_parts)
299 .take_while(|(a, b)| a == b)
300 .count();
301
302 let up_count = (current_parts.len() - common_len) + nesting;
305
306 let down_parts = &target_parts[common_len..];
308
309 let mut segments: Vec<&str> = vec!["super"; up_count];
310 segments.extend_from_slice(down_parts);
311
312 let mut result = segments.join("::");
314 if !result.is_empty() {
315 result.push_str("::");
316 }
317 result.push_str(type_suffix);
318
319 Some(result)
320 }
321
322 pub fn rust_type_relative_split(
334 &self,
335 proto_fqn: &str,
336 current_package: &str,
337 nesting: usize,
338 ) -> Option<SplitPath> {
339 let full_path = self.type_map.get(proto_fqn)?;
340
341 let target_package = self
342 .package_of
343 .get(proto_fqn)
344 .map(|s| s.as_str())
345 .unwrap_or("");
346
347 let target_rust_module = if full_path.starts_with("::") || full_path.starts_with("crate::")
353 {
354 let fqn_no_dot = proto_fqn.strip_prefix('.').unwrap_or(proto_fqn);
363 let within_proto = if target_package.is_empty() {
364 fqn_no_dot
365 } else {
366 fqn_no_dot
367 .strip_prefix(target_package)
368 .and_then(|s| s.strip_prefix('.'))
369 .unwrap_or(fqn_no_dot)
370 };
371 let within_segs = within_proto.split('.').count();
375 let full_segs: Vec<&str> = full_path.split("::").collect();
376 debug_assert!(
381 full_segs.len() >= within_segs,
382 "extern path '{full_path}' has fewer segments than \
383 within-package proto path '{within_proto}'"
384 );
385 let cut = full_segs.len().saturating_sub(within_segs);
386 full_segs[..cut].join("::")
387 } else {
388 target_package.replace('.', "::")
389 };
390
391 let type_suffix = if target_rust_module.is_empty() {
392 full_path.as_str()
393 } else {
394 full_path
395 .strip_prefix(&format!("{}::", target_rust_module))
396 .unwrap_or(full_path)
397 };
398
399 if full_path.starts_with("::") || full_path.starts_with("crate::") {
401 return Some(SplitPath {
402 to_package: target_rust_module,
403 within_package: type_suffix.to_string(),
404 is_extern: true,
405 });
406 }
407
408 if current_package == target_package {
409 let to_package = if nesting == 0 {
410 String::new()
411 } else {
412 (0..nesting).map(|_| "super").collect::<Vec<_>>().join("::")
413 };
414 return Some(SplitPath {
415 to_package,
416 within_package: type_suffix.to_string(),
417 is_extern: false,
418 });
419 }
420
421 let current_parts: Vec<&str> = if current_package.is_empty() {
423 vec![]
424 } else {
425 current_package.split('.').collect()
426 };
427 let target_parts: Vec<&str> = if target_package.is_empty() {
428 vec![]
429 } else {
430 target_package.split('.').collect()
431 };
432 let common_len = current_parts
433 .iter()
434 .zip(&target_parts)
435 .take_while(|(a, b)| a == b)
436 .count();
437 let up_count = (current_parts.len() - common_len) + nesting;
438 let down_parts = &target_parts[common_len..];
439
440 let mut segments: Vec<&str> = vec!["super"; up_count];
441 segments.extend_from_slice(down_parts);
442
443 Some(SplitPath {
444 to_package: segments.join("::"),
445 within_package: type_suffix.to_string(),
446 is_extern: false,
447 })
448 }
449
450 pub(crate) fn matching_attributes(
463 attrs: &[(String, String)],
464 fqn: &str,
465 ) -> Result<proc_macro2::TokenStream, crate::CodeGenError> {
466 if attrs.is_empty() {
467 return Ok(proc_macro2::TokenStream::new());
468 }
469 let fqn_dotted = format!(".{fqn}");
470 let mut tokens = proc_macro2::TokenStream::new();
471 for (prefix, attr_str) in attrs {
472 if matches_proto_prefix(prefix, &fqn_dotted) {
473 let parsed =
474 syn::parse_str::<proc_macro2::TokenStream>(attr_str).map_err(|err| {
475 crate::CodeGenError::InvalidCustomAttribute {
476 path: prefix.clone(),
477 attribute: attr_str.clone(),
478 detail: err.to_string(),
479 }
480 })?;
481 tokens.extend(parsed);
482 }
483 }
484 Ok(tokens)
485 }
486
487 pub fn use_bytes_type(&self, field_fqn: &str) -> bool {
495 self.config
496 .bytes_fields
497 .iter()
498 .any(|prefix| matches_proto_prefix(prefix, field_fqn))
499 }
500}
501
502#[derive(Clone, Copy)]
509pub(crate) struct MessageScope<'a> {
510 pub ctx: &'a CodeGenContext<'a>,
512 pub current_package: &'a str,
514 pub proto_fqn: &'a str,
517 pub features: &'a ResolvedFeatures,
519 pub nesting: usize,
523}
524
525impl<'a> MessageScope<'a> {
526 pub fn nested(&self, proto_fqn: &'a str, features: &'a ResolvedFeatures) -> MessageScope<'a> {
528 MessageScope {
529 ctx: self.ctx,
530 current_package: self.current_package,
531 proto_fqn,
532 features,
533 nesting: self.nesting + 1,
534 }
535 }
536}
537
538#[derive(Debug, Clone, Copy, PartialEq, Eq)]
543pub(crate) enum AncillaryKind {
544 Oneof,
546 View,
548 ViewOneof,
550}
551
552impl AncillaryKind {
553 fn path_segments(self) -> &'static [&'static str] {
554 match self {
555 Self::Oneof => &["oneof"],
556 Self::View => &["view"],
557 Self::ViewOneof => &["view", "oneof"],
558 }
559 }
560}
561
562pub(crate) fn ancillary_prefix(
577 kind: AncillaryKind,
578 current_package: &str,
579 proto_fqn: &str,
580 from_nesting: usize,
581) -> proc_macro2::TokenStream {
582 use crate::idents::make_field_ident;
583 use quote::quote;
584
585 debug_assert!(
586 !proto_fqn.starts_with('.'),
587 "ancillary_prefix expects dotless FQN, got {proto_fqn:?}"
588 );
589
590 let mut supers_tokens = proc_macro2::TokenStream::new();
591 for _ in 0..from_nesting {
592 supers_tokens.extend(quote! { super:: });
593 }
594
595 let sentinel = make_field_ident(SENTINEL_MOD);
596 let kind_segs: Vec<_> = kind
597 .path_segments()
598 .iter()
599 .map(|s| make_field_ident(s))
600 .collect();
601
602 let within_pkg = if current_package.is_empty() {
604 proto_fqn
605 } else {
606 proto_fqn
607 .strip_prefix(current_package)
608 .and_then(|s| s.strip_prefix('.'))
609 .unwrap_or(proto_fqn)
610 };
611 let msg_segs: Vec<_> = within_pkg
612 .split('.')
613 .filter(|s| !s.is_empty())
614 .map(|name| make_field_ident(&to_snake_case(name)))
615 .collect();
616
617 quote! { #supers_tokens #sentinel :: #(#kind_segs ::)* #(#msg_segs ::)* }
618}
619
620pub(crate) fn matches_proto_prefix(prefix: &str, fqn_dotted: &str) -> bool {
625 prefix == "."
626 || prefix == fqn_dotted
627 || (fqn_dotted.starts_with(prefix)
628 && fqn_dotted.as_bytes().get(prefix.len()) == Some(&b'.'))
629}
630
631fn resolve_extern_prefix(package: &str, extern_paths: &[(String, String)]) -> Option<String> {
638 let dotted = format!(".{}", package);
639
640 let mut best: Option<(&str, &str, usize)> = None;
643
644 for (proto_prefix, rust_prefix) in extern_paths {
645 if dotted == *proto_prefix {
646 return Some(rust_prefix.clone());
648 }
649 if let Some(rest) = dotted.strip_prefix(proto_prefix.as_str()) {
650 if proto_prefix == "." || rest.starts_with('.') {
652 let prefix_len = proto_prefix.len();
653 if best.is_none_or(|(_, _, best_len)| prefix_len > best_len) {
654 best = Some((proto_prefix, rust_prefix, prefix_len));
655 }
656 }
657 }
658 }
659
660 let (proto_prefix, rust_prefix, _) = best?;
661 let rest = dotted.strip_prefix(proto_prefix)?;
662 let rest = rest.strip_prefix('.').unwrap_or(rest);
663 let suffix = rest
664 .split('.')
665 .map(to_snake_case)
666 .collect::<Vec<_>>()
667 .join("::");
668 Some(format!("{}::{}", rust_prefix, suffix))
669}
670
671fn register_nested_types(
676 type_map: &mut HashMap<String, String>,
677 package_of: &mut HashMap<String, String>,
678 package: &str,
679 parent_fqn: &str,
680 parent_mod: &str,
681 msg: &crate::generated::descriptor::DescriptorProto,
682) {
683 for nested in &msg.nested_type {
684 if let Some(name) = &nested.name {
685 let fqn = format!("{}.{}", parent_fqn, name);
686 let rust_path = format!("{}::{}", parent_mod, name);
687 type_map.insert(fqn.clone(), rust_path);
688 package_of.insert(fqn.clone(), package.to_string());
689
690 let child_mod = format!("{}::{}", parent_mod, to_snake_case(name));
692 register_nested_types(type_map, package_of, package, &fqn, &child_mod, nested);
693 }
694 }
695
696 for enum_type in &msg.enum_type {
697 if let Some(name) = &enum_type.name {
698 let fqn = format!("{}.{}", parent_fqn, name);
699 let rust_path = format!("{}::{}", parent_mod, name);
700 type_map.insert(fqn.clone(), rust_path);
701 package_of.insert(fqn, package.to_string());
702 }
703 }
704}
705
706fn register_enum_closedness(
708 map: &mut HashMap<String, bool>,
709 fqn: &str,
710 parent_features: &ResolvedFeatures,
711 enum_desc: &EnumDescriptorProto,
712) {
713 let resolved = features::resolve_child(parent_features, features::enum_features(enum_desc));
714 let closed = resolved.enum_type == features::EnumType::Closed;
715 map.insert(fqn.to_string(), closed);
716}
717
718fn register_nested_enum_closedness(
721 map: &mut HashMap<String, bool>,
722 parent_fqn: &str,
723 parent_features: &ResolvedFeatures,
724 msg: &DescriptorProto,
725) {
726 let msg_features = features::resolve_child(parent_features, features::message_features(msg));
727 for enum_type in &msg.enum_type {
728 if let Some(name) = &enum_type.name {
729 let fqn = format!("{}.{}", parent_fqn, name);
730 register_enum_closedness(map, &fqn, &msg_features, enum_type);
731 }
732 }
733 for nested in &msg.nested_type {
734 if let Some(name) = &nested.name {
735 let fqn = format!("{}.{}", parent_fqn, name);
736 register_nested_enum_closedness(map, &fqn, &msg_features, nested);
737 }
738 }
739}
740
741#[cfg(test)]
742mod tests {
743 use super::*;
744 use crate::generated::descriptor::{DescriptorProto, EnumDescriptorProto, FileDescriptorProto};
745
746 fn make_file(
747 name: &str,
748 package: &str,
749 messages: Vec<DescriptorProto>,
750 enums: Vec<EnumDescriptorProto>,
751 ) -> FileDescriptorProto {
752 FileDescriptorProto {
753 name: Some(name.to_string()),
754 package: if package.is_empty() {
755 None
756 } else {
757 Some(package.to_string())
758 },
759 message_type: messages,
760 enum_type: enums,
761 ..Default::default()
762 }
763 }
764
765 fn msg(name: &str) -> DescriptorProto {
766 DescriptorProto {
767 name: Some(name.to_string()),
768 ..Default::default()
769 }
770 }
771
772 fn msg_with_nested(name: &str, nested: Vec<DescriptorProto>) -> DescriptorProto {
773 DescriptorProto {
774 name: Some(name.to_string()),
775 nested_type: nested,
776 ..Default::default()
777 }
778 }
779
780 fn msg_with_nested_and_enums(
781 name: &str,
782 nested: Vec<DescriptorProto>,
783 enums: Vec<EnumDescriptorProto>,
784 ) -> DescriptorProto {
785 DescriptorProto {
786 name: Some(name.to_string()),
787 nested_type: nested,
788 enum_type: enums,
789 ..Default::default()
790 }
791 }
792
793 fn enum_desc(name: &str) -> EnumDescriptorProto {
794 EnumDescriptorProto {
795 name: Some(name.to_string()),
796 ..Default::default()
797 }
798 }
799
800 fn enum_with_closed_feature(name: &str) -> EnumDescriptorProto {
801 use crate::generated::descriptor::{feature_set, EnumOptions, FeatureSet};
802 EnumDescriptorProto {
803 name: Some(name.to_string()),
804 options: buffa::MessageField::some(EnumOptions {
805 features: buffa::MessageField::some(FeatureSet {
806 enum_type: Some(feature_set::EnumType::CLOSED),
807 ..Default::default()
808 }),
809 ..Default::default()
810 }),
811 ..Default::default()
812 }
813 }
814
815 fn editions_file(
816 name: &str,
817 package: &str,
818 messages: Vec<DescriptorProto>,
819 enums: Vec<EnumDescriptorProto>,
820 ) -> FileDescriptorProto {
821 use crate::generated::descriptor::Edition;
822 FileDescriptorProto {
823 name: Some(name.to_string()),
824 package: Some(package.to_string()),
825 syntax: Some("editions".to_string()),
826 edition: Some(Edition::EDITION_2023),
827 message_type: messages,
828 enum_type: enums,
829 ..Default::default()
830 }
831 }
832
833 #[test]
836 fn test_message_with_package() {
837 let files = [make_file(
838 "test.proto",
839 "my.package",
840 vec![msg("Foo")],
841 vec![],
842 )];
843 let config = CodeGenConfig::default();
844 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
845 assert_eq!(ctx.rust_type(".my.package.Foo"), Some("my::package::Foo"));
846 }
847
848 #[test]
849 fn test_message_no_package() {
850 let files = [make_file("test.proto", "", vec![msg("Bar")], vec![])];
851 let config = CodeGenConfig::default();
852 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
853 assert_eq!(ctx.rust_type(".Bar"), Some("Bar"));
854 }
855
856 #[test]
857 fn test_nested_message_uses_module_path() {
858 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
859 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
860 let config = CodeGenConfig::default();
861 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
862 assert_eq!(ctx.rust_type(".pkg.Outer"), Some("pkg::Outer"));
863 assert_eq!(ctx.rust_type(".pkg.Outer.Inner"), Some("pkg::outer::Inner"));
865 }
866
867 #[test]
868 fn test_nested_message_no_package() {
869 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
870 let files = [make_file("test.proto", "", vec![outer], vec![])];
871 let config = CodeGenConfig::default();
872 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
873 assert_eq!(ctx.rust_type(".Outer"), Some("Outer"));
874 assert_eq!(ctx.rust_type(".Outer.Inner"), Some("outer::Inner"));
875 }
876
877 #[test]
878 fn test_deeply_nested_message() {
879 let deep = msg_with_nested("A", vec![msg_with_nested("B", vec![msg("C")])]);
880 let files = [make_file("test.proto", "pkg", vec![deep], vec![])];
881 let config = CodeGenConfig::default();
882 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
883 assert_eq!(ctx.rust_type(".pkg.A"), Some("pkg::A"));
884 assert_eq!(ctx.rust_type(".pkg.A.B"), Some("pkg::a::B"));
885 assert_eq!(ctx.rust_type(".pkg.A.B.C"), Some("pkg::a::b::C"));
886 }
887
888 #[test]
889 fn test_nested_enum_uses_module_path() {
890 let outer = msg_with_nested_and_enums("Outer", vec![], vec![enum_desc("Status")]);
891 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
892 let config = CodeGenConfig::default();
893 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
894 assert_eq!(
895 ctx.rust_type(".pkg.Outer.Status"),
896 Some("pkg::outer::Status")
897 );
898 }
899
900 #[test]
901 fn test_top_level_enum() {
902 let files = [make_file(
903 "test.proto",
904 "pkg",
905 vec![],
906 vec![enum_desc("Status")],
907 )];
908 let config = CodeGenConfig::default();
909 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
910 assert_eq!(ctx.rust_type(".pkg.Status"), Some("pkg::Status"));
911 }
912
913 #[test]
914 fn test_same_named_nested_types_in_different_parents_are_distinct() {
915 let outer1 = msg_with_nested("Outer1", vec![msg("Inner")]);
916 let outer2 = msg_with_nested("Outer2", vec![msg("Inner")]);
917 let files = [make_file("a.proto", "pkg", vec![outer1, outer2], vec![])];
918 let config = CodeGenConfig::default();
919 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
920 assert_eq!(
922 ctx.rust_type(".pkg.Outer1.Inner"),
923 Some("pkg::outer1::Inner")
924 );
925 assert_eq!(
926 ctx.rust_type(".pkg.Outer2.Inner"),
927 Some("pkg::outer2::Inner")
928 );
929 assert_ne!(
930 ctx.rust_type(".pkg.Outer1.Inner"),
931 ctx.rust_type(".pkg.Outer2.Inner")
932 );
933 }
934
935 #[test]
936 fn test_multiple_files() {
937 let files = [
938 make_file("a.proto", "ns.a", vec![msg("MsgA")], vec![]),
939 make_file("b.proto", "ns.b", vec![msg("MsgB")], vec![]),
940 ];
941 let config = CodeGenConfig::default();
942 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
943 assert_eq!(ctx.rust_type(".ns.a.MsgA"), Some("ns::a::MsgA"));
944 assert_eq!(ctx.rust_type(".ns.b.MsgB"), Some("ns::b::MsgB"));
945 }
946
947 #[test]
948 fn test_keyword_package_segment_in_type_map() {
949 let files = [make_file(
952 "latlng.proto",
953 "google.type",
954 vec![msg("LatLng")],
955 vec![],
956 )];
957 let config = CodeGenConfig::default();
958 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
959 assert_eq!(
960 ctx.rust_type(".google.type.LatLng"),
961 Some("google::type::LatLng")
962 );
963 }
964
965 #[test]
966 fn test_keyword_package_relative_same_package() {
967 let files = [make_file(
968 "latlng.proto",
969 "google.type",
970 vec![msg("LatLng"), msg("Expr")],
971 vec![],
972 )];
973 let config = CodeGenConfig::default();
974 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
975 assert_eq!(
977 ctx.rust_type_relative(".google.type.LatLng", "google.type", 0),
978 Some("LatLng".into())
979 );
980 }
981
982 #[test]
983 fn test_keyword_package_cross_package() {
984 let files = [
985 make_file("latlng.proto", "google.type", vec![msg("LatLng")], vec![]),
986 make_file("svc.proto", "google.cloud", vec![msg("Service")], vec![]),
987 ];
988 let config = CodeGenConfig::default();
989 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
990 assert_eq!(
993 ctx.rust_type_relative(".google.type.LatLng", "google.cloud", 0),
994 Some("super::type::LatLng".into())
995 );
996 }
997
998 #[test]
999 fn test_keyword_nested_message_module() {
1000 let outer = msg_with_nested("Type", vec![msg("Inner")]);
1002 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
1003 let config = CodeGenConfig::default();
1004 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1005 assert_eq!(ctx.rust_type(".pkg.Type"), Some("pkg::Type"));
1006 assert_eq!(ctx.rust_type(".pkg.Type.Inner"), Some("pkg::type::Inner"));
1007 }
1008
1009 #[test]
1010 fn test_unknown_type_returns_none() {
1011 let files = [make_file("test.proto", "pkg", vec![msg("Foo")], vec![])];
1012 let config = CodeGenConfig::default();
1013 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1014 assert_eq!(ctx.rust_type(".pkg.Unknown"), None);
1015 }
1016
1017 #[test]
1020 fn test_relative_same_package_top_level() {
1021 let files = [make_file("a.proto", "pkg", vec![msg("Foo")], vec![])];
1022 let config = CodeGenConfig::default();
1023 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1024 assert_eq!(
1026 ctx.rust_type_relative(".pkg.Foo", "pkg", 0),
1027 Some("Foo".into())
1028 );
1029 }
1030
1031 #[test]
1032 fn test_relative_cross_package() {
1033 let files = [
1034 make_file("a.proto", "pkg_a", vec![msg("Foo")], vec![]),
1035 make_file("b.proto", "pkg_b", vec![msg("Bar")], vec![]),
1036 ];
1037 let config = CodeGenConfig::default();
1038 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1039 assert_eq!(
1041 ctx.rust_type_relative(".pkg_a.Foo", "pkg_b", 0),
1042 Some("super::pkg_a::Foo".into())
1043 );
1044 }
1045
1046 #[test]
1047 fn test_relative_no_package() {
1048 let files = [make_file("a.proto", "", vec![msg("Foo")], vec![])];
1049 let config = CodeGenConfig::default();
1050 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1051 assert_eq!(ctx.rust_type_relative(".Foo", "", 0), Some("Foo".into()));
1052 }
1053
1054 #[test]
1055 fn test_relative_unknown_returns_none() {
1056 let files = [make_file("a.proto", "pkg", vec![msg("Foo")], vec![])];
1057 let config = CodeGenConfig::default();
1058 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1059 assert_eq!(ctx.rust_type_relative(".pkg.Unknown", "pkg", 0), None);
1060 }
1061
1062 #[test]
1063 fn test_relative_dotted_package() {
1064 let files = [make_file("a.proto", "my.pkg", vec![msg("Foo")], vec![])];
1065 let config = CodeGenConfig::default();
1066 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1067 assert_eq!(
1068 ctx.rust_type_relative(".my.pkg.Foo", "my.pkg", 0),
1069 Some("Foo".into())
1070 );
1071 }
1072
1073 #[test]
1074 fn test_relative_cross_dotted_packages() {
1075 let files = [
1076 make_file(
1077 "timestamp.proto",
1078 "google.protobuf",
1079 vec![msg("Timestamp")],
1080 vec![],
1081 ),
1082 make_file(
1083 "test.proto",
1084 "protobuf_test_messages.proto3",
1085 vec![msg("TestAllTypesProto3")],
1086 vec![],
1087 ),
1088 ];
1089 let config = CodeGenConfig::default();
1090 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1091
1092 assert_eq!(
1094 ctx.rust_type_relative(
1095 ".google.protobuf.Timestamp",
1096 "protobuf_test_messages.proto3",
1097 0,
1098 ),
1099 Some("super::super::google::protobuf::Timestamp".into())
1100 );
1101 }
1102
1103 #[test]
1104 fn test_relative_nested_type_from_same_package() {
1105 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
1107 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
1108 let config = CodeGenConfig::default();
1109 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1110
1111 assert_eq!(
1113 ctx.rust_type_relative(".pkg.Outer.Inner", "pkg", 0),
1114 Some("outer::Inner".into())
1115 );
1116 }
1117
1118 #[test]
1119 fn test_relative_shared_prefix_not_confused() {
1120 let files = [
1121 make_file("ab.proto", "a.b", vec![msg("Msg1")], vec![]),
1122 make_file("abc.proto", "a.bc", vec![msg("Msg2")], vec![]),
1123 ];
1124 let config = CodeGenConfig::default();
1125 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1126
1127 assert_eq!(
1129 ctx.rust_type_relative(".a.b.Msg1", "a.bc", 0),
1130 Some("super::b::Msg1".into())
1131 );
1132 assert_eq!(
1134 ctx.rust_type_relative(".a.bc.Msg2", "a.b", 0),
1135 Some("super::bc::Msg2".into())
1136 );
1137 }
1138
1139 #[test]
1142 fn test_relative_cross_package_nesting_1() {
1143 let outer = msg_with_nested_and_enums("Business", vec![], vec![enum_desc("Status")]);
1147 let files = [
1148 make_file("admin.proto", "a.b.admin.v1", vec![msg("Svc")], vec![]),
1149 make_file("biz.proto", "a.b.v1", vec![outer], vec![]),
1150 ];
1151 let config = CodeGenConfig::default();
1152 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1153
1154 assert_eq!(
1156 ctx.rust_type_relative(".a.b.v1.Business.Status", "a.b.admin.v1", 0),
1157 Some("super::super::v1::business::Status".into())
1158 );
1159 assert_eq!(
1161 ctx.rust_type_relative(".a.b.v1.Business.Status", "a.b.admin.v1", 1),
1162 Some("super::super::super::v1::business::Status".into())
1163 );
1164 }
1165
1166 #[test]
1167 fn test_relative_same_package_nesting_1() {
1168 let files = [make_file(
1170 "test.proto",
1171 "pkg",
1172 vec![msg("Foo"), msg("Bar")],
1173 vec![],
1174 )];
1175 let config = CodeGenConfig::default();
1176 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1177
1178 assert_eq!(
1180 ctx.rust_type_relative(".pkg.Foo", "pkg", 0),
1181 Some("Foo".into())
1182 );
1183 assert_eq!(
1185 ctx.rust_type_relative(".pkg.Foo", "pkg", 1),
1186 Some("super::Foo".into())
1187 );
1188 assert_eq!(
1190 ctx.rust_type_relative(".pkg.Foo", "pkg", 2),
1191 Some("super::super::Foo".into())
1192 );
1193 }
1194
1195 #[test]
1198 fn test_resolve_extern_prefix_exact_match() {
1199 let result = resolve_extern_prefix(
1200 "my.common",
1201 &[(".my.common".into(), "::common_protos".into())],
1202 );
1203 assert_eq!(result, Some("::common_protos".into()));
1204 }
1205
1206 #[test]
1207 fn test_resolve_extern_prefix_sub_package() {
1208 let result = resolve_extern_prefix(
1209 "my.common.sub",
1210 &[(".my.common".into(), "::common_protos".into())],
1211 );
1212 assert_eq!(result, Some("::common_protos::sub".into()));
1213 }
1214
1215 #[test]
1216 fn test_resolve_extern_prefix_no_match() {
1217 let result = resolve_extern_prefix(
1218 "other.pkg",
1219 &[(".my.common".into(), "::common_protos".into())],
1220 );
1221 assert_eq!(result, None);
1222 }
1223
1224 #[test]
1225 fn test_resolve_extern_prefix_partial_name_no_match() {
1226 let result = resolve_extern_prefix(
1228 "my.commonext",
1229 &[(".my.common".into(), "::common_protos".into())],
1230 );
1231 assert_eq!(result, None);
1232 }
1233
1234 #[test]
1235 fn test_resolve_extern_prefix_longest_match_wins() {
1236 let result = resolve_extern_prefix(
1238 "my.common.sub",
1239 &[
1240 (".my".into(), "::crate_a".into()),
1241 (".my.common".into(), "::crate_b".into()),
1242 ],
1243 );
1244 assert_eq!(result, Some("::crate_b::sub".into()));
1245 }
1246
1247 #[test]
1248 fn test_resolve_extern_prefix_catchall() {
1249 let result = resolve_extern_prefix("greet.v1", &[(".".into(), "crate::proto".into())]);
1250 assert_eq!(result, Some("crate::proto::greet::v1".into()));
1251 }
1252
1253 #[test]
1254 fn test_resolve_extern_prefix_catchall_empty_pkg() {
1255 let result = resolve_extern_prefix("", &[(".".into(), "crate::proto".into())]);
1258 assert_eq!(result, Some("crate::proto".into()));
1259 }
1260
1261 #[test]
1262 fn test_resolve_extern_prefix_catchall_longest_wins() {
1263 let result = resolve_extern_prefix(
1266 "google.protobuf",
1267 &[
1268 (".".into(), "crate::proto".into()),
1269 (
1270 ".google.protobuf".into(),
1271 "::buffa_types::google::protobuf".into(),
1272 ),
1273 ],
1274 );
1275 assert_eq!(result, Some("::buffa_types::google::protobuf".into()));
1276 }
1277
1278 #[test]
1279 fn test_resolve_extern_prefix_catchall_keyword_package() {
1280 let result = resolve_extern_prefix("google.type", &[(".".into(), "crate::proto".into())]);
1283 assert_eq!(result, Some("crate::proto::google::type".into()));
1284 }
1285
1286 #[test]
1289 fn test_split_extern_top_level() {
1290 let outer = msg_with_nested("Value", vec![msg("Inner")]);
1291 let files = [make_file(
1292 "struct.proto",
1293 "google.protobuf",
1294 vec![outer],
1295 vec![],
1296 )];
1297 let config = CodeGenConfig::default();
1298 let extern_paths = vec![(
1299 ".google.protobuf".into(),
1300 "::buffa_types::google::protobuf".into(),
1301 )];
1302 let ctx = CodeGenContext::new(&files, &config, &extern_paths);
1303
1304 let split = ctx
1305 .rust_type_relative_split(".google.protobuf.Value", "my.pkg", 3)
1306 .expect("type resolves");
1307 assert!(split.is_extern);
1308 assert_eq!(split.to_package, "::buffa_types::google::protobuf");
1310 assert_eq!(split.within_package, "Value");
1311 }
1312
1313 #[test]
1314 fn test_split_extern_nested_type() {
1315 let outer = msg_with_nested("Value", vec![msg("Inner")]);
1320 let files = [make_file(
1321 "struct.proto",
1322 "google.protobuf",
1323 vec![outer],
1324 vec![],
1325 )];
1326 let config = CodeGenConfig::default();
1327 let extern_paths = vec![(
1328 ".google.protobuf".into(),
1329 "::buffa_types::google::protobuf".into(),
1330 )];
1331 let ctx = CodeGenContext::new(&files, &config, &extern_paths);
1332
1333 let split = ctx
1334 .rust_type_relative_split(".google.protobuf.Value.Inner", "my.pkg", 0)
1335 .expect("nested type resolves");
1336 assert!(split.is_extern);
1337 assert_eq!(split.to_package, "::buffa_types::google::protobuf");
1338 assert_eq!(split.within_package, "value::Inner");
1339 }
1340
1341 #[test]
1342 fn test_extern_path_top_level_message() {
1343 let files = [make_file(
1344 "common.proto",
1345 "my.common",
1346 vec![msg("SharedMsg")],
1347 vec![],
1348 )];
1349 let config = CodeGenConfig {
1350 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
1351 ..Default::default()
1352 };
1353 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1354 assert_eq!(
1355 ctx.rust_type(".my.common.SharedMsg"),
1356 Some("::common_protos::SharedMsg")
1357 );
1358 }
1359
1360 #[test]
1361 fn test_extern_path_nested_message() {
1362 let files = [make_file(
1363 "common.proto",
1364 "my.common",
1365 vec![msg_with_nested("Outer", vec![msg("Inner")])],
1366 vec![],
1367 )];
1368 let config = CodeGenConfig {
1369 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
1370 ..Default::default()
1371 };
1372 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1373 assert_eq!(
1374 ctx.rust_type(".my.common.Outer"),
1375 Some("::common_protos::Outer")
1376 );
1377 assert_eq!(
1378 ctx.rust_type(".my.common.Outer.Inner"),
1379 Some("::common_protos::outer::Inner")
1380 );
1381 }
1382
1383 #[test]
1384 fn test_extern_path_enum() {
1385 let files = [make_file(
1386 "common.proto",
1387 "my.common",
1388 vec![],
1389 vec![enum_desc("Status")],
1390 )];
1391 let config = CodeGenConfig {
1392 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
1393 ..Default::default()
1394 };
1395 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1396 assert_eq!(
1397 ctx.rust_type(".my.common.Status"),
1398 Some("::common_protos::Status")
1399 );
1400 }
1401
1402 #[test]
1403 fn test_extern_path_does_not_affect_other_packages() {
1404 let files = [
1405 make_file("common.proto", "my.common", vec![msg("SharedMsg")], vec![]),
1406 make_file(
1407 "service.proto",
1408 "my.service",
1409 vec![msg("MyService")],
1410 vec![],
1411 ),
1412 ];
1413 let config = CodeGenConfig {
1414 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
1415 ..Default::default()
1416 };
1417 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1418 assert_eq!(
1420 ctx.rust_type(".my.common.SharedMsg"),
1421 Some("::common_protos::SharedMsg")
1422 );
1423 assert_eq!(
1425 ctx.rust_type(".my.service.MyService"),
1426 Some("my::service::MyService")
1427 );
1428 }
1429
1430 #[test]
1431 fn test_extern_path_relative_returns_absolute() {
1432 let files = [
1435 make_file("common.proto", "my.common", vec![msg("SharedMsg")], vec![]),
1436 make_file(
1437 "service.proto",
1438 "my.service",
1439 vec![msg("MyService")],
1440 vec![],
1441 ),
1442 ];
1443 let config = CodeGenConfig {
1444 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
1445 ..Default::default()
1446 };
1447 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1448 assert_eq!(
1450 ctx.rust_type_relative(".my.common.SharedMsg", "my.service", 0),
1451 Some("::common_protos::SharedMsg".into())
1452 );
1453 }
1454
1455 #[test]
1458 fn test_is_enum_closed_proto3_default_open() {
1459 let files = [make_file("a.proto", "p", vec![], vec![enum_desc("E")])];
1460 let config = CodeGenConfig::default();
1461 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1462 assert_eq!(ctx.is_enum_closed(".p.E"), Some(true));
1466 }
1467
1468 #[test]
1469 fn test_is_enum_closed_editions_default_open() {
1470 let files = [editions_file("a.proto", "p", vec![], vec![enum_desc("E")])];
1471 let config = CodeGenConfig::default();
1472 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1473 assert_eq!(ctx.is_enum_closed(".p.E"), Some(false));
1475 }
1476
1477 #[test]
1478 fn test_is_enum_closed_per_enum_override() {
1479 let files = [editions_file(
1482 "a.proto",
1483 "p",
1484 vec![],
1485 vec![enum_desc("Open"), enum_with_closed_feature("Closed")],
1486 )];
1487 let config = CodeGenConfig::default();
1488 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1489 assert_eq!(ctx.is_enum_closed(".p.Open"), Some(false));
1490 assert_eq!(ctx.is_enum_closed(".p.Closed"), Some(true));
1491 }
1492
1493 #[test]
1494 fn test_is_enum_closed_nested_per_enum_override() {
1495 let files = [editions_file(
1497 "a.proto",
1498 "p",
1499 vec![msg_with_nested_and_enums(
1500 "M",
1501 vec![],
1502 vec![enum_with_closed_feature("Inner")],
1503 )],
1504 vec![],
1505 )];
1506 let config = CodeGenConfig::default();
1507 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1508 assert_eq!(ctx.is_enum_closed(".p.M.Inner"), Some(true));
1509 }
1510
1511 #[test]
1512 fn test_is_enum_closed_unknown_enum_returns_none() {
1513 let files = [editions_file("a.proto", "p", vec![], vec![])];
1514 let config = CodeGenConfig::default();
1515 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1516 assert_eq!(ctx.is_enum_closed(".other.Unknown"), None);
1518 }
1519
1520 #[test]
1521 fn test_for_generate_auto_injects_wkt_mapping() {
1522 let ts_msg = DescriptorProto {
1525 name: Some("Timestamp".into()),
1526 ..Default::default()
1527 };
1528 let files = [FileDescriptorProto {
1529 name: Some("google/protobuf/timestamp.proto".into()),
1530 package: Some("google.protobuf".into()),
1531 syntax: Some("proto3".into()),
1532 message_type: vec![ts_msg],
1533 ..Default::default()
1534 }];
1535 let config = CodeGenConfig::default();
1536 let ctx = CodeGenContext::for_generate(&files, &["other.proto".into()], &config);
1538 assert_eq!(
1539 ctx.rust_type(".google.protobuf.Timestamp"),
1540 Some("::buffa_types::google::protobuf::Timestamp"),
1541 "WKT auto-mapping must be applied via for_generate"
1542 );
1543 }
1544
1545 #[test]
1546 fn test_for_generate_suppresses_wkt_when_generating_wkt() {
1547 let ts_msg = DescriptorProto {
1550 name: Some("Timestamp".into()),
1551 ..Default::default()
1552 };
1553 let files = [FileDescriptorProto {
1554 name: Some("google/protobuf/timestamp.proto".into()),
1555 package: Some("google.protobuf".into()),
1556 syntax: Some("proto3".into()),
1557 message_type: vec![ts_msg],
1558 ..Default::default()
1559 }];
1560 let config = CodeGenConfig::default();
1561 let ctx = CodeGenContext::for_generate(
1562 &files,
1563 &["google/protobuf/timestamp.proto".into()],
1564 &config,
1565 );
1566 assert_eq!(
1568 ctx.rust_type(".google.protobuf.Timestamp"),
1569 Some("google::protobuf::Timestamp")
1570 );
1571 }
1572
1573 #[test]
1576 fn test_matching_attributes_catchall() {
1577 let attrs = vec![(".".into(), "#[derive(Foo)]".into())];
1579 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
1580 assert!(result.to_string().contains("derive"));
1581 }
1582
1583 #[test]
1584 fn test_matching_attributes_exact_match() {
1585 let attrs = vec![(".my.pkg.MyMessage".into(), "#[derive(Bar)]".into())];
1586 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
1587 assert!(result.to_string().contains("derive"));
1588 }
1589
1590 #[test]
1591 fn test_matching_attributes_package_prefix() {
1592 let attrs = vec![(".my.pkg".into(), "#[derive(Baz)]".into())];
1593 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
1594 assert!(result.to_string().contains("derive"));
1595 }
1596
1597 #[test]
1598 fn test_matching_attributes_no_partial_segment_match() {
1599 let attrs = vec![(".my.pk".into(), "#[derive(Bad)]".into())];
1601 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
1602 assert!(result.is_empty());
1603 }
1604
1605 #[test]
1606 fn test_matching_attributes_no_match() {
1607 let attrs = vec![(".other.pkg".into(), "#[derive(Nope)]".into())];
1608 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
1609 assert!(result.is_empty());
1610 }
1611
1612 #[test]
1613 fn test_matching_attributes_multiple_accumulate() {
1614 let attrs = vec![
1616 (".".into(), "#[derive(A)]".into()),
1617 (".my.pkg".into(), "#[derive(B)]".into()),
1618 ];
1619 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
1620 let s = result.to_string();
1621 assert!(s.contains("A") && s.contains("B"));
1622 }
1623
1624 #[test]
1625 fn test_matching_attributes_invalid_attr_errors() {
1626 let attrs = vec![(".".into(), "not valid {{{{".into())];
1629 let err = CodeGenContext::matching_attributes(&attrs, "my.pkg.Msg").unwrap_err();
1630 assert!(matches!(
1631 err,
1632 crate::CodeGenError::InvalidCustomAttribute { .. }
1633 ));
1634 }
1635
1636 #[test]
1637 fn test_matches_proto_prefix_catchall() {
1638 assert!(matches_proto_prefix(".", ".anything.here"));
1639 assert!(matches_proto_prefix(".", "."));
1640 }
1641
1642 #[test]
1643 fn test_matches_proto_prefix_segment_boundary() {
1644 assert!(!matches_proto_prefix(".my.pk", ".my.pkg.Msg"));
1646 assert!(matches_proto_prefix(".my.pkg", ".my.pkg.Msg"));
1648 }
1649}