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 ViewOneof,
548}
549
550impl AncillaryKind {
551 fn path_segments(self) -> &'static [&'static str] {
552 match self {
553 Self::Oneof => &["oneof"],
554 Self::ViewOneof => &["view", "oneof"],
555 }
556 }
557}
558
559pub(crate) fn ancillary_prefix(
574 kind: AncillaryKind,
575 current_package: &str,
576 proto_fqn: &str,
577 from_nesting: usize,
578) -> proc_macro2::TokenStream {
579 use crate::idents::make_field_ident;
580 use quote::quote;
581
582 debug_assert!(
583 !proto_fqn.starts_with('.'),
584 "ancillary_prefix expects dotless FQN, got {proto_fqn:?}"
585 );
586
587 let mut supers_tokens = proc_macro2::TokenStream::new();
588 for _ in 0..from_nesting {
589 supers_tokens.extend(quote! { super:: });
590 }
591
592 let sentinel = make_field_ident(SENTINEL_MOD);
593 let kind_segs: Vec<_> = kind
594 .path_segments()
595 .iter()
596 .map(|s| make_field_ident(s))
597 .collect();
598
599 let within_pkg = if current_package.is_empty() {
601 proto_fqn
602 } else {
603 proto_fqn
604 .strip_prefix(current_package)
605 .and_then(|s| s.strip_prefix('.'))
606 .unwrap_or(proto_fqn)
607 };
608 let msg_segs: Vec<_> = within_pkg
609 .split('.')
610 .filter(|s| !s.is_empty())
611 .map(|name| make_field_ident(&to_snake_case(name)))
612 .collect();
613
614 quote! { #supers_tokens #sentinel :: #(#kind_segs ::)* #(#msg_segs ::)* }
615}
616
617pub(crate) fn matches_proto_prefix(prefix: &str, fqn_dotted: &str) -> bool {
622 prefix == "."
623 || prefix == fqn_dotted
624 || (fqn_dotted.starts_with(prefix)
625 && fqn_dotted.as_bytes().get(prefix.len()) == Some(&b'.'))
626}
627
628fn resolve_extern_prefix(package: &str, extern_paths: &[(String, String)]) -> Option<String> {
635 let dotted = format!(".{}", package);
636
637 let mut best: Option<(&str, &str, usize)> = None;
640
641 for (proto_prefix, rust_prefix) in extern_paths {
642 if dotted == *proto_prefix {
643 return Some(rust_prefix.clone());
645 }
646 if let Some(rest) = dotted.strip_prefix(proto_prefix.as_str()) {
647 if proto_prefix == "." || rest.starts_with('.') {
649 let prefix_len = proto_prefix.len();
650 if best.is_none_or(|(_, _, best_len)| prefix_len > best_len) {
651 best = Some((proto_prefix, rust_prefix, prefix_len));
652 }
653 }
654 }
655 }
656
657 let (proto_prefix, rust_prefix, _) = best?;
658 let rest = dotted.strip_prefix(proto_prefix)?;
659 let rest = rest.strip_prefix('.').unwrap_or(rest);
660 let suffix = rest
661 .split('.')
662 .map(to_snake_case)
663 .collect::<Vec<_>>()
664 .join("::");
665 Some(format!("{}::{}", rust_prefix, suffix))
666}
667
668fn register_nested_types(
673 type_map: &mut HashMap<String, String>,
674 package_of: &mut HashMap<String, String>,
675 package: &str,
676 parent_fqn: &str,
677 parent_mod: &str,
678 msg: &crate::generated::descriptor::DescriptorProto,
679) {
680 for nested in &msg.nested_type {
681 if let Some(name) = &nested.name {
682 let fqn = format!("{}.{}", parent_fqn, name);
683 let rust_path = format!("{}::{}", parent_mod, name);
684 type_map.insert(fqn.clone(), rust_path);
685 package_of.insert(fqn.clone(), package.to_string());
686
687 let child_mod = format!("{}::{}", parent_mod, to_snake_case(name));
689 register_nested_types(type_map, package_of, package, &fqn, &child_mod, nested);
690 }
691 }
692
693 for enum_type in &msg.enum_type {
694 if let Some(name) = &enum_type.name {
695 let fqn = format!("{}.{}", parent_fqn, name);
696 let rust_path = format!("{}::{}", parent_mod, name);
697 type_map.insert(fqn.clone(), rust_path);
698 package_of.insert(fqn, package.to_string());
699 }
700 }
701}
702
703fn register_enum_closedness(
705 map: &mut HashMap<String, bool>,
706 fqn: &str,
707 parent_features: &ResolvedFeatures,
708 enum_desc: &EnumDescriptorProto,
709) {
710 let resolved = features::resolve_child(parent_features, features::enum_features(enum_desc));
711 let closed = resolved.enum_type == features::EnumType::Closed;
712 map.insert(fqn.to_string(), closed);
713}
714
715fn register_nested_enum_closedness(
718 map: &mut HashMap<String, bool>,
719 parent_fqn: &str,
720 parent_features: &ResolvedFeatures,
721 msg: &DescriptorProto,
722) {
723 let msg_features = features::resolve_child(parent_features, features::message_features(msg));
724 for enum_type in &msg.enum_type {
725 if let Some(name) = &enum_type.name {
726 let fqn = format!("{}.{}", parent_fqn, name);
727 register_enum_closedness(map, &fqn, &msg_features, enum_type);
728 }
729 }
730 for nested in &msg.nested_type {
731 if let Some(name) = &nested.name {
732 let fqn = format!("{}.{}", parent_fqn, name);
733 register_nested_enum_closedness(map, &fqn, &msg_features, nested);
734 }
735 }
736}
737
738#[cfg(test)]
739mod tests {
740 use super::*;
741 use crate::generated::descriptor::{DescriptorProto, EnumDescriptorProto, FileDescriptorProto};
742
743 fn make_file(
744 name: &str,
745 package: &str,
746 messages: Vec<DescriptorProto>,
747 enums: Vec<EnumDescriptorProto>,
748 ) -> FileDescriptorProto {
749 FileDescriptorProto {
750 name: Some(name.to_string()),
751 package: if package.is_empty() {
752 None
753 } else {
754 Some(package.to_string())
755 },
756 message_type: messages,
757 enum_type: enums,
758 ..Default::default()
759 }
760 }
761
762 fn msg(name: &str) -> DescriptorProto {
763 DescriptorProto {
764 name: Some(name.to_string()),
765 ..Default::default()
766 }
767 }
768
769 fn msg_with_nested(name: &str, nested: Vec<DescriptorProto>) -> DescriptorProto {
770 DescriptorProto {
771 name: Some(name.to_string()),
772 nested_type: nested,
773 ..Default::default()
774 }
775 }
776
777 fn msg_with_nested_and_enums(
778 name: &str,
779 nested: Vec<DescriptorProto>,
780 enums: Vec<EnumDescriptorProto>,
781 ) -> DescriptorProto {
782 DescriptorProto {
783 name: Some(name.to_string()),
784 nested_type: nested,
785 enum_type: enums,
786 ..Default::default()
787 }
788 }
789
790 fn enum_desc(name: &str) -> EnumDescriptorProto {
791 EnumDescriptorProto {
792 name: Some(name.to_string()),
793 ..Default::default()
794 }
795 }
796
797 fn enum_with_closed_feature(name: &str) -> EnumDescriptorProto {
798 use crate::generated::descriptor::{feature_set, EnumOptions, FeatureSet};
799 EnumDescriptorProto {
800 name: Some(name.to_string()),
801 options: buffa::MessageField::some(EnumOptions {
802 features: buffa::MessageField::some(FeatureSet {
803 enum_type: Some(feature_set::EnumType::CLOSED),
804 ..Default::default()
805 }),
806 ..Default::default()
807 }),
808 ..Default::default()
809 }
810 }
811
812 fn editions_file(
813 name: &str,
814 package: &str,
815 messages: Vec<DescriptorProto>,
816 enums: Vec<EnumDescriptorProto>,
817 ) -> FileDescriptorProto {
818 use crate::generated::descriptor::Edition;
819 FileDescriptorProto {
820 name: Some(name.to_string()),
821 package: Some(package.to_string()),
822 syntax: Some("editions".to_string()),
823 edition: Some(Edition::EDITION_2023),
824 message_type: messages,
825 enum_type: enums,
826 ..Default::default()
827 }
828 }
829
830 #[test]
833 fn test_message_with_package() {
834 let files = [make_file(
835 "test.proto",
836 "my.package",
837 vec![msg("Foo")],
838 vec![],
839 )];
840 let config = CodeGenConfig::default();
841 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
842 assert_eq!(ctx.rust_type(".my.package.Foo"), Some("my::package::Foo"));
843 }
844
845 #[test]
846 fn test_message_no_package() {
847 let files = [make_file("test.proto", "", vec![msg("Bar")], vec![])];
848 let config = CodeGenConfig::default();
849 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
850 assert_eq!(ctx.rust_type(".Bar"), Some("Bar"));
851 }
852
853 #[test]
854 fn test_nested_message_uses_module_path() {
855 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
856 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
857 let config = CodeGenConfig::default();
858 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
859 assert_eq!(ctx.rust_type(".pkg.Outer"), Some("pkg::Outer"));
860 assert_eq!(ctx.rust_type(".pkg.Outer.Inner"), Some("pkg::outer::Inner"));
862 }
863
864 #[test]
865 fn test_nested_message_no_package() {
866 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
867 let files = [make_file("test.proto", "", vec![outer], vec![])];
868 let config = CodeGenConfig::default();
869 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
870 assert_eq!(ctx.rust_type(".Outer"), Some("Outer"));
871 assert_eq!(ctx.rust_type(".Outer.Inner"), Some("outer::Inner"));
872 }
873
874 #[test]
875 fn test_deeply_nested_message() {
876 let deep = msg_with_nested("A", vec![msg_with_nested("B", vec![msg("C")])]);
877 let files = [make_file("test.proto", "pkg", vec![deep], vec![])];
878 let config = CodeGenConfig::default();
879 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
880 assert_eq!(ctx.rust_type(".pkg.A"), Some("pkg::A"));
881 assert_eq!(ctx.rust_type(".pkg.A.B"), Some("pkg::a::B"));
882 assert_eq!(ctx.rust_type(".pkg.A.B.C"), Some("pkg::a::b::C"));
883 }
884
885 #[test]
886 fn test_nested_enum_uses_module_path() {
887 let outer = msg_with_nested_and_enums("Outer", vec![], vec![enum_desc("Status")]);
888 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
889 let config = CodeGenConfig::default();
890 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
891 assert_eq!(
892 ctx.rust_type(".pkg.Outer.Status"),
893 Some("pkg::outer::Status")
894 );
895 }
896
897 #[test]
898 fn test_top_level_enum() {
899 let files = [make_file(
900 "test.proto",
901 "pkg",
902 vec![],
903 vec![enum_desc("Status")],
904 )];
905 let config = CodeGenConfig::default();
906 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
907 assert_eq!(ctx.rust_type(".pkg.Status"), Some("pkg::Status"));
908 }
909
910 #[test]
911 fn test_same_named_nested_types_in_different_parents_are_distinct() {
912 let outer1 = msg_with_nested("Outer1", vec![msg("Inner")]);
913 let outer2 = msg_with_nested("Outer2", vec![msg("Inner")]);
914 let files = [make_file("a.proto", "pkg", vec![outer1, outer2], vec![])];
915 let config = CodeGenConfig::default();
916 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
917 assert_eq!(
919 ctx.rust_type(".pkg.Outer1.Inner"),
920 Some("pkg::outer1::Inner")
921 );
922 assert_eq!(
923 ctx.rust_type(".pkg.Outer2.Inner"),
924 Some("pkg::outer2::Inner")
925 );
926 assert_ne!(
927 ctx.rust_type(".pkg.Outer1.Inner"),
928 ctx.rust_type(".pkg.Outer2.Inner")
929 );
930 }
931
932 #[test]
933 fn test_multiple_files() {
934 let files = [
935 make_file("a.proto", "ns.a", vec![msg("MsgA")], vec![]),
936 make_file("b.proto", "ns.b", vec![msg("MsgB")], vec![]),
937 ];
938 let config = CodeGenConfig::default();
939 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
940 assert_eq!(ctx.rust_type(".ns.a.MsgA"), Some("ns::a::MsgA"));
941 assert_eq!(ctx.rust_type(".ns.b.MsgB"), Some("ns::b::MsgB"));
942 }
943
944 #[test]
945 fn test_keyword_package_segment_in_type_map() {
946 let files = [make_file(
949 "latlng.proto",
950 "google.type",
951 vec![msg("LatLng")],
952 vec![],
953 )];
954 let config = CodeGenConfig::default();
955 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
956 assert_eq!(
957 ctx.rust_type(".google.type.LatLng"),
958 Some("google::type::LatLng")
959 );
960 }
961
962 #[test]
963 fn test_keyword_package_relative_same_package() {
964 let files = [make_file(
965 "latlng.proto",
966 "google.type",
967 vec![msg("LatLng"), msg("Expr")],
968 vec![],
969 )];
970 let config = CodeGenConfig::default();
971 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
972 assert_eq!(
974 ctx.rust_type_relative(".google.type.LatLng", "google.type", 0),
975 Some("LatLng".into())
976 );
977 }
978
979 #[test]
980 fn test_keyword_package_cross_package() {
981 let files = [
982 make_file("latlng.proto", "google.type", vec![msg("LatLng")], vec![]),
983 make_file("svc.proto", "google.cloud", vec![msg("Service")], vec![]),
984 ];
985 let config = CodeGenConfig::default();
986 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
987 assert_eq!(
990 ctx.rust_type_relative(".google.type.LatLng", "google.cloud", 0),
991 Some("super::type::LatLng".into())
992 );
993 }
994
995 #[test]
996 fn test_keyword_nested_message_module() {
997 let outer = msg_with_nested("Type", vec![msg("Inner")]);
999 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
1000 let config = CodeGenConfig::default();
1001 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1002 assert_eq!(ctx.rust_type(".pkg.Type"), Some("pkg::Type"));
1003 assert_eq!(ctx.rust_type(".pkg.Type.Inner"), Some("pkg::type::Inner"));
1004 }
1005
1006 #[test]
1007 fn test_unknown_type_returns_none() {
1008 let files = [make_file("test.proto", "pkg", vec![msg("Foo")], vec![])];
1009 let config = CodeGenConfig::default();
1010 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1011 assert_eq!(ctx.rust_type(".pkg.Unknown"), None);
1012 }
1013
1014 #[test]
1017 fn test_relative_same_package_top_level() {
1018 let files = [make_file("a.proto", "pkg", vec![msg("Foo")], vec![])];
1019 let config = CodeGenConfig::default();
1020 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1021 assert_eq!(
1023 ctx.rust_type_relative(".pkg.Foo", "pkg", 0),
1024 Some("Foo".into())
1025 );
1026 }
1027
1028 #[test]
1029 fn test_relative_cross_package() {
1030 let files = [
1031 make_file("a.proto", "pkg_a", vec![msg("Foo")], vec![]),
1032 make_file("b.proto", "pkg_b", vec![msg("Bar")], vec![]),
1033 ];
1034 let config = CodeGenConfig::default();
1035 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1036 assert_eq!(
1038 ctx.rust_type_relative(".pkg_a.Foo", "pkg_b", 0),
1039 Some("super::pkg_a::Foo".into())
1040 );
1041 }
1042
1043 #[test]
1044 fn test_relative_no_package() {
1045 let files = [make_file("a.proto", "", vec![msg("Foo")], vec![])];
1046 let config = CodeGenConfig::default();
1047 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1048 assert_eq!(ctx.rust_type_relative(".Foo", "", 0), Some("Foo".into()));
1049 }
1050
1051 #[test]
1052 fn test_relative_unknown_returns_none() {
1053 let files = [make_file("a.proto", "pkg", vec![msg("Foo")], vec![])];
1054 let config = CodeGenConfig::default();
1055 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1056 assert_eq!(ctx.rust_type_relative(".pkg.Unknown", "pkg", 0), None);
1057 }
1058
1059 #[test]
1060 fn test_relative_dotted_package() {
1061 let files = [make_file("a.proto", "my.pkg", vec![msg("Foo")], vec![])];
1062 let config = CodeGenConfig::default();
1063 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1064 assert_eq!(
1065 ctx.rust_type_relative(".my.pkg.Foo", "my.pkg", 0),
1066 Some("Foo".into())
1067 );
1068 }
1069
1070 #[test]
1071 fn test_relative_cross_dotted_packages() {
1072 let files = [
1073 make_file(
1074 "timestamp.proto",
1075 "google.protobuf",
1076 vec![msg("Timestamp")],
1077 vec![],
1078 ),
1079 make_file(
1080 "test.proto",
1081 "protobuf_test_messages.proto3",
1082 vec![msg("TestAllTypesProto3")],
1083 vec![],
1084 ),
1085 ];
1086 let config = CodeGenConfig::default();
1087 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1088
1089 assert_eq!(
1091 ctx.rust_type_relative(
1092 ".google.protobuf.Timestamp",
1093 "protobuf_test_messages.proto3",
1094 0,
1095 ),
1096 Some("super::super::google::protobuf::Timestamp".into())
1097 );
1098 }
1099
1100 #[test]
1101 fn test_relative_nested_type_from_same_package() {
1102 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
1104 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
1105 let config = CodeGenConfig::default();
1106 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1107
1108 assert_eq!(
1110 ctx.rust_type_relative(".pkg.Outer.Inner", "pkg", 0),
1111 Some("outer::Inner".into())
1112 );
1113 }
1114
1115 #[test]
1116 fn test_relative_shared_prefix_not_confused() {
1117 let files = [
1118 make_file("ab.proto", "a.b", vec![msg("Msg1")], vec![]),
1119 make_file("abc.proto", "a.bc", vec![msg("Msg2")], vec![]),
1120 ];
1121 let config = CodeGenConfig::default();
1122 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1123
1124 assert_eq!(
1126 ctx.rust_type_relative(".a.b.Msg1", "a.bc", 0),
1127 Some("super::b::Msg1".into())
1128 );
1129 assert_eq!(
1131 ctx.rust_type_relative(".a.bc.Msg2", "a.b", 0),
1132 Some("super::bc::Msg2".into())
1133 );
1134 }
1135
1136 #[test]
1139 fn test_relative_cross_package_nesting_1() {
1140 let outer = msg_with_nested_and_enums("Business", vec![], vec![enum_desc("Status")]);
1144 let files = [
1145 make_file("admin.proto", "a.b.admin.v1", vec![msg("Svc")], vec![]),
1146 make_file("biz.proto", "a.b.v1", vec![outer], vec![]),
1147 ];
1148 let config = CodeGenConfig::default();
1149 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1150
1151 assert_eq!(
1153 ctx.rust_type_relative(".a.b.v1.Business.Status", "a.b.admin.v1", 0),
1154 Some("super::super::v1::business::Status".into())
1155 );
1156 assert_eq!(
1158 ctx.rust_type_relative(".a.b.v1.Business.Status", "a.b.admin.v1", 1),
1159 Some("super::super::super::v1::business::Status".into())
1160 );
1161 }
1162
1163 #[test]
1164 fn test_relative_same_package_nesting_1() {
1165 let files = [make_file(
1167 "test.proto",
1168 "pkg",
1169 vec![msg("Foo"), msg("Bar")],
1170 vec![],
1171 )];
1172 let config = CodeGenConfig::default();
1173 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1174
1175 assert_eq!(
1177 ctx.rust_type_relative(".pkg.Foo", "pkg", 0),
1178 Some("Foo".into())
1179 );
1180 assert_eq!(
1182 ctx.rust_type_relative(".pkg.Foo", "pkg", 1),
1183 Some("super::Foo".into())
1184 );
1185 assert_eq!(
1187 ctx.rust_type_relative(".pkg.Foo", "pkg", 2),
1188 Some("super::super::Foo".into())
1189 );
1190 }
1191
1192 #[test]
1195 fn test_resolve_extern_prefix_exact_match() {
1196 let result = resolve_extern_prefix(
1197 "my.common",
1198 &[(".my.common".into(), "::common_protos".into())],
1199 );
1200 assert_eq!(result, Some("::common_protos".into()));
1201 }
1202
1203 #[test]
1204 fn test_resolve_extern_prefix_sub_package() {
1205 let result = resolve_extern_prefix(
1206 "my.common.sub",
1207 &[(".my.common".into(), "::common_protos".into())],
1208 );
1209 assert_eq!(result, Some("::common_protos::sub".into()));
1210 }
1211
1212 #[test]
1213 fn test_resolve_extern_prefix_no_match() {
1214 let result = resolve_extern_prefix(
1215 "other.pkg",
1216 &[(".my.common".into(), "::common_protos".into())],
1217 );
1218 assert_eq!(result, None);
1219 }
1220
1221 #[test]
1222 fn test_resolve_extern_prefix_partial_name_no_match() {
1223 let result = resolve_extern_prefix(
1225 "my.commonext",
1226 &[(".my.common".into(), "::common_protos".into())],
1227 );
1228 assert_eq!(result, None);
1229 }
1230
1231 #[test]
1232 fn test_resolve_extern_prefix_longest_match_wins() {
1233 let result = resolve_extern_prefix(
1235 "my.common.sub",
1236 &[
1237 (".my".into(), "::crate_a".into()),
1238 (".my.common".into(), "::crate_b".into()),
1239 ],
1240 );
1241 assert_eq!(result, Some("::crate_b::sub".into()));
1242 }
1243
1244 #[test]
1245 fn test_resolve_extern_prefix_catchall() {
1246 let result = resolve_extern_prefix("greet.v1", &[(".".into(), "crate::proto".into())]);
1247 assert_eq!(result, Some("crate::proto::greet::v1".into()));
1248 }
1249
1250 #[test]
1251 fn test_resolve_extern_prefix_catchall_empty_pkg() {
1252 let result = resolve_extern_prefix("", &[(".".into(), "crate::proto".into())]);
1255 assert_eq!(result, Some("crate::proto".into()));
1256 }
1257
1258 #[test]
1259 fn test_resolve_extern_prefix_catchall_longest_wins() {
1260 let result = resolve_extern_prefix(
1263 "google.protobuf",
1264 &[
1265 (".".into(), "crate::proto".into()),
1266 (
1267 ".google.protobuf".into(),
1268 "::buffa_types::google::protobuf".into(),
1269 ),
1270 ],
1271 );
1272 assert_eq!(result, Some("::buffa_types::google::protobuf".into()));
1273 }
1274
1275 #[test]
1276 fn test_resolve_extern_prefix_catchall_keyword_package() {
1277 let result = resolve_extern_prefix("google.type", &[(".".into(), "crate::proto".into())]);
1280 assert_eq!(result, Some("crate::proto::google::type".into()));
1281 }
1282
1283 #[test]
1286 fn test_split_extern_top_level() {
1287 let outer = msg_with_nested("Value", vec![msg("Inner")]);
1288 let files = [make_file(
1289 "struct.proto",
1290 "google.protobuf",
1291 vec![outer],
1292 vec![],
1293 )];
1294 let config = CodeGenConfig::default();
1295 let extern_paths = vec![(
1296 ".google.protobuf".into(),
1297 "::buffa_types::google::protobuf".into(),
1298 )];
1299 let ctx = CodeGenContext::new(&files, &config, &extern_paths);
1300
1301 let split = ctx
1302 .rust_type_relative_split(".google.protobuf.Value", "my.pkg", 3)
1303 .expect("type resolves");
1304 assert!(split.is_extern);
1305 assert_eq!(split.to_package, "::buffa_types::google::protobuf");
1307 assert_eq!(split.within_package, "Value");
1308 }
1309
1310 #[test]
1311 fn test_split_extern_nested_type() {
1312 let outer = msg_with_nested("Value", vec![msg("Inner")]);
1317 let files = [make_file(
1318 "struct.proto",
1319 "google.protobuf",
1320 vec![outer],
1321 vec![],
1322 )];
1323 let config = CodeGenConfig::default();
1324 let extern_paths = vec![(
1325 ".google.protobuf".into(),
1326 "::buffa_types::google::protobuf".into(),
1327 )];
1328 let ctx = CodeGenContext::new(&files, &config, &extern_paths);
1329
1330 let split = ctx
1331 .rust_type_relative_split(".google.protobuf.Value.Inner", "my.pkg", 0)
1332 .expect("nested type resolves");
1333 assert!(split.is_extern);
1334 assert_eq!(split.to_package, "::buffa_types::google::protobuf");
1335 assert_eq!(split.within_package, "value::Inner");
1336 }
1337
1338 #[test]
1339 fn test_extern_path_top_level_message() {
1340 let files = [make_file(
1341 "common.proto",
1342 "my.common",
1343 vec![msg("SharedMsg")],
1344 vec![],
1345 )];
1346 let config = CodeGenConfig {
1347 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
1348 ..Default::default()
1349 };
1350 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1351 assert_eq!(
1352 ctx.rust_type(".my.common.SharedMsg"),
1353 Some("::common_protos::SharedMsg")
1354 );
1355 }
1356
1357 #[test]
1358 fn test_extern_path_nested_message() {
1359 let files = [make_file(
1360 "common.proto",
1361 "my.common",
1362 vec![msg_with_nested("Outer", vec![msg("Inner")])],
1363 vec![],
1364 )];
1365 let config = CodeGenConfig {
1366 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
1367 ..Default::default()
1368 };
1369 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1370 assert_eq!(
1371 ctx.rust_type(".my.common.Outer"),
1372 Some("::common_protos::Outer")
1373 );
1374 assert_eq!(
1375 ctx.rust_type(".my.common.Outer.Inner"),
1376 Some("::common_protos::outer::Inner")
1377 );
1378 }
1379
1380 #[test]
1381 fn test_extern_path_enum() {
1382 let files = [make_file(
1383 "common.proto",
1384 "my.common",
1385 vec![],
1386 vec![enum_desc("Status")],
1387 )];
1388 let config = CodeGenConfig {
1389 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
1390 ..Default::default()
1391 };
1392 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1393 assert_eq!(
1394 ctx.rust_type(".my.common.Status"),
1395 Some("::common_protos::Status")
1396 );
1397 }
1398
1399 #[test]
1400 fn test_extern_path_does_not_affect_other_packages() {
1401 let files = [
1402 make_file("common.proto", "my.common", vec![msg("SharedMsg")], vec![]),
1403 make_file(
1404 "service.proto",
1405 "my.service",
1406 vec![msg("MyService")],
1407 vec![],
1408 ),
1409 ];
1410 let config = CodeGenConfig {
1411 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
1412 ..Default::default()
1413 };
1414 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1415 assert_eq!(
1417 ctx.rust_type(".my.common.SharedMsg"),
1418 Some("::common_protos::SharedMsg")
1419 );
1420 assert_eq!(
1422 ctx.rust_type(".my.service.MyService"),
1423 Some("my::service::MyService")
1424 );
1425 }
1426
1427 #[test]
1428 fn test_extern_path_relative_returns_absolute() {
1429 let files = [
1432 make_file("common.proto", "my.common", vec![msg("SharedMsg")], vec![]),
1433 make_file(
1434 "service.proto",
1435 "my.service",
1436 vec![msg("MyService")],
1437 vec![],
1438 ),
1439 ];
1440 let config = CodeGenConfig {
1441 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
1442 ..Default::default()
1443 };
1444 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1445 assert_eq!(
1447 ctx.rust_type_relative(".my.common.SharedMsg", "my.service", 0),
1448 Some("::common_protos::SharedMsg".into())
1449 );
1450 }
1451
1452 #[test]
1455 fn test_is_enum_closed_proto3_default_open() {
1456 let files = [make_file("a.proto", "p", vec![], vec![enum_desc("E")])];
1457 let config = CodeGenConfig::default();
1458 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1459 assert_eq!(ctx.is_enum_closed(".p.E"), Some(true));
1463 }
1464
1465 #[test]
1466 fn test_is_enum_closed_editions_default_open() {
1467 let files = [editions_file("a.proto", "p", vec![], vec![enum_desc("E")])];
1468 let config = CodeGenConfig::default();
1469 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1470 assert_eq!(ctx.is_enum_closed(".p.E"), Some(false));
1472 }
1473
1474 #[test]
1475 fn test_is_enum_closed_per_enum_override() {
1476 let files = [editions_file(
1479 "a.proto",
1480 "p",
1481 vec![],
1482 vec![enum_desc("Open"), enum_with_closed_feature("Closed")],
1483 )];
1484 let config = CodeGenConfig::default();
1485 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1486 assert_eq!(ctx.is_enum_closed(".p.Open"), Some(false));
1487 assert_eq!(ctx.is_enum_closed(".p.Closed"), Some(true));
1488 }
1489
1490 #[test]
1491 fn test_is_enum_closed_nested_per_enum_override() {
1492 let files = [editions_file(
1494 "a.proto",
1495 "p",
1496 vec![msg_with_nested_and_enums(
1497 "M",
1498 vec![],
1499 vec![enum_with_closed_feature("Inner")],
1500 )],
1501 vec![],
1502 )];
1503 let config = CodeGenConfig::default();
1504 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1505 assert_eq!(ctx.is_enum_closed(".p.M.Inner"), Some(true));
1506 }
1507
1508 #[test]
1509 fn test_is_enum_closed_unknown_enum_returns_none() {
1510 let files = [editions_file("a.proto", "p", vec![], vec![])];
1511 let config = CodeGenConfig::default();
1512 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1513 assert_eq!(ctx.is_enum_closed(".other.Unknown"), None);
1515 }
1516
1517 #[test]
1518 fn test_for_generate_auto_injects_wkt_mapping() {
1519 let ts_msg = DescriptorProto {
1522 name: Some("Timestamp".into()),
1523 ..Default::default()
1524 };
1525 let files = [FileDescriptorProto {
1526 name: Some("google/protobuf/timestamp.proto".into()),
1527 package: Some("google.protobuf".into()),
1528 syntax: Some("proto3".into()),
1529 message_type: vec![ts_msg],
1530 ..Default::default()
1531 }];
1532 let config = CodeGenConfig::default();
1533 let ctx = CodeGenContext::for_generate(&files, &["other.proto".into()], &config);
1535 assert_eq!(
1536 ctx.rust_type(".google.protobuf.Timestamp"),
1537 Some("::buffa_types::google::protobuf::Timestamp"),
1538 "WKT auto-mapping must be applied via for_generate"
1539 );
1540 }
1541
1542 #[test]
1543 fn test_for_generate_suppresses_wkt_when_generating_wkt() {
1544 let ts_msg = DescriptorProto {
1547 name: Some("Timestamp".into()),
1548 ..Default::default()
1549 };
1550 let files = [FileDescriptorProto {
1551 name: Some("google/protobuf/timestamp.proto".into()),
1552 package: Some("google.protobuf".into()),
1553 syntax: Some("proto3".into()),
1554 message_type: vec![ts_msg],
1555 ..Default::default()
1556 }];
1557 let config = CodeGenConfig::default();
1558 let ctx = CodeGenContext::for_generate(
1559 &files,
1560 &["google/protobuf/timestamp.proto".into()],
1561 &config,
1562 );
1563 assert_eq!(
1565 ctx.rust_type(".google.protobuf.Timestamp"),
1566 Some("google::protobuf::Timestamp")
1567 );
1568 }
1569
1570 #[test]
1573 fn test_matching_attributes_catchall() {
1574 let attrs = vec![(".".into(), "#[derive(Foo)]".into())];
1576 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
1577 assert!(result.to_string().contains("derive"));
1578 }
1579
1580 #[test]
1581 fn test_matching_attributes_exact_match() {
1582 let attrs = vec![(".my.pkg.MyMessage".into(), "#[derive(Bar)]".into())];
1583 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
1584 assert!(result.to_string().contains("derive"));
1585 }
1586
1587 #[test]
1588 fn test_matching_attributes_package_prefix() {
1589 let attrs = vec![(".my.pkg".into(), "#[derive(Baz)]".into())];
1590 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
1591 assert!(result.to_string().contains("derive"));
1592 }
1593
1594 #[test]
1595 fn test_matching_attributes_no_partial_segment_match() {
1596 let attrs = vec![(".my.pk".into(), "#[derive(Bad)]".into())];
1598 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
1599 assert!(result.is_empty());
1600 }
1601
1602 #[test]
1603 fn test_matching_attributes_no_match() {
1604 let attrs = vec![(".other.pkg".into(), "#[derive(Nope)]".into())];
1605 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
1606 assert!(result.is_empty());
1607 }
1608
1609 #[test]
1610 fn test_matching_attributes_multiple_accumulate() {
1611 let attrs = vec![
1613 (".".into(), "#[derive(A)]".into()),
1614 (".my.pkg".into(), "#[derive(B)]".into()),
1615 ];
1616 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
1617 let s = result.to_string();
1618 assert!(s.contains("A") && s.contains("B"));
1619 }
1620
1621 #[test]
1622 fn test_matching_attributes_invalid_attr_errors() {
1623 let attrs = vec![(".".into(), "not valid {{{{".into())];
1626 let err = CodeGenContext::matching_attributes(&attrs, "my.pkg.Msg").unwrap_err();
1627 assert!(matches!(
1628 err,
1629 crate::CodeGenError::InvalidCustomAttribute { .. }
1630 ));
1631 }
1632
1633 #[test]
1634 fn test_matches_proto_prefix_catchall() {
1635 assert!(matches_proto_prefix(".", ".anything.here"));
1636 assert!(matches_proto_prefix(".", "."));
1637 }
1638
1639 #[test]
1640 fn test_matches_proto_prefix_segment_boundary() {
1641 assert!(!matches_proto_prefix(".my.pk", ".my.pkg.Msg"));
1643 assert!(matches_proto_prefix(".my.pkg", ".my.pkg.Msg"));
1645 }
1646}