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(
98 files: &'a [FileDescriptorProto],
99 config: &'a CodeGenConfig,
100 effective_extern_paths: &[(String, String)],
101 ) -> Self {
102 Self::with_extern_resolution(files, config, effective_extern_paths, &[])
103 }
104
105 pub(crate) fn with_extern_resolution(
123 files: &'a [FileDescriptorProto],
124 config: &'a CodeGenConfig,
125 effective_extern_paths: &[(String, String)],
126 file_extern_paths: &[(String, String)],
127 ) -> Self {
128 let mut type_map = HashMap::new();
129 let mut package_of = HashMap::new();
130 let mut enum_closedness = HashMap::new();
131 let mut comment_map = HashMap::new();
132
133 for file in files {
134 comment_map.extend(crate::comments::fqn_comments(file));
135 let package = file.package.as_deref().unwrap_or("");
136 let file_features = features::for_file(file);
137 let proto_prefix = if package.is_empty() {
138 String::from(".")
139 } else {
140 format!(".{}.", package)
141 };
142
143 let rust_module = if let Some(rust_root) = file
149 .name
150 .as_deref()
151 .and_then(|n| resolve_file_extern(n, file_extern_paths))
152 {
153 rust_root.to_string()
154 } else if let Some(rust_root) = resolve_extern_prefix(package, effective_extern_paths) {
155 rust_root
156 } else {
157 package.replace('.', "::")
158 };
159
160 for msg in &file.message_type {
162 if let Some(name) = &msg.name {
163 let fqn = format!("{}{}", proto_prefix, name);
164 let rust_path = if rust_module.is_empty() {
165 name.clone()
166 } else {
167 format!("{}::{}", rust_module, name)
168 };
169 type_map.insert(fqn.clone(), rust_path);
170 package_of.insert(fqn.clone(), package.to_string());
171
172 let snake = to_snake_case(name);
174 let parent_mod = if rust_module.is_empty() {
175 snake
176 } else {
177 format!("{}::{}", rust_module, snake)
178 };
179 register_nested_types(
180 &mut type_map,
181 &mut package_of,
182 package,
183 &fqn,
184 &parent_mod,
185 msg,
186 );
187 register_nested_enum_closedness(
188 &mut enum_closedness,
189 &fqn,
190 &file_features,
191 msg,
192 );
193 }
194 }
195
196 for enum_type in &file.enum_type {
198 if let Some(name) = &enum_type.name {
199 let fqn = format!("{}{}", proto_prefix, name);
200 let rust_path = if rust_module.is_empty() {
201 name.clone()
202 } else {
203 format!("{}::{}", rust_module, name)
204 };
205 type_map.insert(fqn.clone(), rust_path);
206 package_of.insert(fqn.clone(), package.to_string());
207 register_enum_closedness(&mut enum_closedness, &fqn, &file_features, enum_type);
208 }
209 }
210 }
211
212 Self {
213 files,
214 config,
215 type_map,
216 package_of,
217 enum_closedness,
218 comment_map,
219 }
220 }
221
222 pub fn for_generate(
236 files: &'a [FileDescriptorProto],
237 files_to_generate: &[String],
238 config: &'a CodeGenConfig,
239 ) -> Self {
240 let paths = crate::effective_extern_paths(files, files_to_generate, config);
241 let file_paths = crate::effective_file_extern_paths(files_to_generate, config);
242 Self::with_extern_resolution(files, config, &paths, &file_paths)
243 }
244
245 pub fn rust_type(&self, proto_fqn: &str) -> Option<&str> {
247 self.type_map.get(proto_fqn).map(|s| s.as_str())
248 }
249
250 pub fn comment(&self, fqn: &str) -> Option<&str> {
259 self.comment_map.get(fqn).map(|s| s.as_str())
260 }
261
262 pub fn is_enum_closed(&self, proto_fqn: &str) -> Option<bool> {
269 self.enum_closedness.get(proto_fqn).copied()
270 }
271
272 pub fn rust_type_relative(
287 &self,
288 proto_fqn: &str,
289 current_package: &str,
290 nesting: usize,
291 ) -> Option<String> {
292 let full_path = self.type_map.get(proto_fqn)?;
293
294 if full_path.starts_with("::") || full_path.starts_with("crate::") {
297 return Some(full_path.clone());
298 }
299
300 let target_package = self
301 .package_of
302 .get(proto_fqn)
303 .map(|s| s.as_str())
304 .unwrap_or("");
305
306 let target_rust_module = target_package.replace('.', "::");
309 let type_suffix = if target_rust_module.is_empty() {
310 full_path.as_str()
311 } else {
312 full_path
313 .strip_prefix(&format!("{}::", target_rust_module))
314 .unwrap_or(full_path)
315 };
316
317 if current_package == target_package {
318 if nesting == 0 {
320 return Some(type_suffix.to_string());
321 }
322 let supers = (0..nesting).map(|_| "super").collect::<Vec<_>>().join("::");
323 return Some(format!("{}::{}", supers, type_suffix));
324 }
325
326 let current_parts: Vec<&str> = if current_package.is_empty() {
328 vec![]
329 } else {
330 current_package.split('.').collect()
331 };
332 let target_parts: Vec<&str> = if target_package.is_empty() {
333 vec![]
334 } else {
335 target_package.split('.').collect()
336 };
337
338 let common_len = current_parts
340 .iter()
341 .zip(&target_parts)
342 .take_while(|(a, b)| a == b)
343 .count();
344
345 let up_count = (current_parts.len() - common_len) + nesting;
348
349 let down_parts = &target_parts[common_len..];
351
352 let mut segments: Vec<&str> = vec!["super"; up_count];
353 segments.extend_from_slice(down_parts);
354
355 let mut result = segments.join("::");
357 if !result.is_empty() {
358 result.push_str("::");
359 }
360 result.push_str(type_suffix);
361
362 Some(result)
363 }
364
365 pub fn rust_type_relative_split(
377 &self,
378 proto_fqn: &str,
379 current_package: &str,
380 nesting: usize,
381 ) -> Option<SplitPath> {
382 let full_path = self.type_map.get(proto_fqn)?;
383
384 let target_package = self
385 .package_of
386 .get(proto_fqn)
387 .map(|s| s.as_str())
388 .unwrap_or("");
389
390 let target_rust_module = if full_path.starts_with("::") || full_path.starts_with("crate::")
396 {
397 let fqn_no_dot = proto_fqn.strip_prefix('.').unwrap_or(proto_fqn);
406 let within_proto = if target_package.is_empty() {
407 fqn_no_dot
408 } else {
409 fqn_no_dot
410 .strip_prefix(target_package)
411 .and_then(|s| s.strip_prefix('.'))
412 .unwrap_or(fqn_no_dot)
413 };
414 let within_segs = within_proto.split('.').count();
418 let full_segs: Vec<&str> = full_path.split("::").collect();
419 debug_assert!(
424 full_segs.len() >= within_segs,
425 "extern path '{full_path}' has fewer segments than \
426 within-package proto path '{within_proto}'"
427 );
428 let cut = full_segs.len().saturating_sub(within_segs);
429 full_segs[..cut].join("::")
430 } else {
431 target_package.replace('.', "::")
432 };
433
434 let type_suffix = if target_rust_module.is_empty() {
435 full_path.as_str()
436 } else {
437 full_path
438 .strip_prefix(&format!("{}::", target_rust_module))
439 .unwrap_or(full_path)
440 };
441
442 if full_path.starts_with("::") || full_path.starts_with("crate::") {
444 return Some(SplitPath {
445 to_package: target_rust_module,
446 within_package: type_suffix.to_string(),
447 is_extern: true,
448 });
449 }
450
451 if current_package == target_package {
452 let to_package = if nesting == 0 {
453 String::new()
454 } else {
455 (0..nesting).map(|_| "super").collect::<Vec<_>>().join("::")
456 };
457 return Some(SplitPath {
458 to_package,
459 within_package: type_suffix.to_string(),
460 is_extern: false,
461 });
462 }
463
464 let current_parts: Vec<&str> = if current_package.is_empty() {
466 vec![]
467 } else {
468 current_package.split('.').collect()
469 };
470 let target_parts: Vec<&str> = if target_package.is_empty() {
471 vec![]
472 } else {
473 target_package.split('.').collect()
474 };
475 let common_len = current_parts
476 .iter()
477 .zip(&target_parts)
478 .take_while(|(a, b)| a == b)
479 .count();
480 let up_count = (current_parts.len() - common_len) + nesting;
481 let down_parts = &target_parts[common_len..];
482
483 let mut segments: Vec<&str> = vec!["super"; up_count];
484 segments.extend_from_slice(down_parts);
485
486 Some(SplitPath {
487 to_package: segments.join("::"),
488 within_package: type_suffix.to_string(),
489 is_extern: false,
490 })
491 }
492
493 pub(crate) fn matching_attributes(
506 attrs: &[(String, String)],
507 fqn: &str,
508 ) -> Result<proc_macro2::TokenStream, crate::CodeGenError> {
509 if attrs.is_empty() {
510 return Ok(proc_macro2::TokenStream::new());
511 }
512 let fqn_dotted = format!(".{fqn}");
513 let mut tokens = proc_macro2::TokenStream::new();
514 for (prefix, attr_str) in attrs {
515 if matches_proto_prefix(prefix, &fqn_dotted) {
516 let parsed =
517 syn::parse_str::<proc_macro2::TokenStream>(attr_str).map_err(|err| {
518 crate::CodeGenError::InvalidCustomAttribute {
519 path: prefix.clone(),
520 attribute: attr_str.clone(),
521 detail: err.to_string(),
522 }
523 })?;
524 tokens.extend(parsed);
525 }
526 }
527 Ok(tokens)
528 }
529
530 pub fn use_bytes_type(&self, field_fqn: &str) -> bool {
538 self.config
539 .bytes_fields
540 .iter()
541 .any(|prefix| matches_proto_prefix(prefix, field_fqn))
542 }
543}
544
545#[derive(Clone, Copy)]
552pub(crate) struct MessageScope<'a> {
553 pub ctx: &'a CodeGenContext<'a>,
555 pub current_package: &'a str,
557 pub proto_fqn: &'a str,
560 pub features: &'a ResolvedFeatures,
562 pub nesting: usize,
566}
567
568impl<'a> MessageScope<'a> {
569 pub fn nested(&self, proto_fqn: &'a str, features: &'a ResolvedFeatures) -> MessageScope<'a> {
571 MessageScope {
572 ctx: self.ctx,
573 current_package: self.current_package,
574 proto_fqn,
575 features,
576 nesting: self.nesting + 1,
577 }
578 }
579}
580
581#[derive(Debug, Clone, Copy, PartialEq, Eq)]
586pub(crate) enum AncillaryKind {
587 Oneof,
589 View,
591 ViewOneof,
593}
594
595impl AncillaryKind {
596 fn path_segments(self) -> &'static [&'static str] {
597 match self {
598 Self::Oneof => &["oneof"],
599 Self::View => &["view"],
600 Self::ViewOneof => &["view", "oneof"],
601 }
602 }
603}
604
605pub(crate) fn ancillary_prefix(
620 kind: AncillaryKind,
621 current_package: &str,
622 proto_fqn: &str,
623 from_nesting: usize,
624) -> proc_macro2::TokenStream {
625 use crate::idents::make_field_ident;
626 use quote::quote;
627
628 debug_assert!(
629 !proto_fqn.starts_with('.'),
630 "ancillary_prefix expects dotless FQN, got {proto_fqn:?}"
631 );
632
633 let mut supers_tokens = proc_macro2::TokenStream::new();
634 for _ in 0..from_nesting {
635 supers_tokens.extend(quote! { super:: });
636 }
637
638 let sentinel = make_field_ident(SENTINEL_MOD);
639 let kind_segs: Vec<_> = kind
640 .path_segments()
641 .iter()
642 .map(|s| make_field_ident(s))
643 .collect();
644
645 let within_pkg = if current_package.is_empty() {
647 proto_fqn
648 } else {
649 proto_fqn
650 .strip_prefix(current_package)
651 .and_then(|s| s.strip_prefix('.'))
652 .unwrap_or(proto_fqn)
653 };
654 let msg_segs: Vec<_> = within_pkg
655 .split('.')
656 .filter(|s| !s.is_empty())
657 .map(|name| make_field_ident(&to_snake_case(name)))
658 .collect();
659
660 quote! { #supers_tokens #sentinel :: #(#kind_segs ::)* #(#msg_segs ::)* }
661}
662
663pub(crate) fn matches_proto_prefix(prefix: &str, fqn_dotted: &str) -> bool {
668 prefix == "."
669 || prefix == fqn_dotted
670 || (fqn_dotted.starts_with(prefix)
671 && fqn_dotted.as_bytes().get(prefix.len()) == Some(&b'.'))
672}
673
674fn resolve_file_extern<'p>(
683 file_name: &str,
684 file_extern_paths: &'p [(String, String)],
685) -> Option<&'p str> {
686 file_extern_paths
687 .iter()
688 .find(|(name, _)| name == file_name)
689 .map(|(_, rust)| rust.as_str())
690}
691
692pub(crate) fn resolve_extern_prefix(
703 package: &str,
704 extern_paths: &[(String, String)],
705) -> Option<String> {
706 let dotted = format!(".{}", package);
707
708 let mut best: Option<(&str, &str, usize)> = None;
711
712 for (proto_prefix, rust_prefix) in extern_paths {
713 if dotted == *proto_prefix {
714 return Some(rust_prefix.clone());
716 }
717 if let Some(rest) = dotted.strip_prefix(proto_prefix.as_str()) {
718 if proto_prefix == "." || rest.starts_with('.') {
720 let prefix_len = proto_prefix.len();
721 if best.is_none_or(|(_, _, best_len)| prefix_len > best_len) {
722 best = Some((proto_prefix, rust_prefix, prefix_len));
723 }
724 }
725 }
726 }
727
728 let (proto_prefix, rust_prefix, _) = best?;
729 let rest = dotted.strip_prefix(proto_prefix)?;
730 let rest = rest.strip_prefix('.').unwrap_or(rest);
731 let suffix = rest
732 .split('.')
733 .map(to_snake_case)
734 .collect::<Vec<_>>()
735 .join("::");
736 Some(format!("{}::{}", rust_prefix, suffix))
737}
738
739fn register_nested_types(
744 type_map: &mut HashMap<String, String>,
745 package_of: &mut HashMap<String, String>,
746 package: &str,
747 parent_fqn: &str,
748 parent_mod: &str,
749 msg: &crate::generated::descriptor::DescriptorProto,
750) {
751 for nested in &msg.nested_type {
752 if let Some(name) = &nested.name {
753 let fqn = format!("{}.{}", parent_fqn, name);
754 let rust_path = format!("{}::{}", parent_mod, name);
755 type_map.insert(fqn.clone(), rust_path);
756 package_of.insert(fqn.clone(), package.to_string());
757
758 let child_mod = format!("{}::{}", parent_mod, to_snake_case(name));
760 register_nested_types(type_map, package_of, package, &fqn, &child_mod, nested);
761 }
762 }
763
764 for enum_type in &msg.enum_type {
765 if let Some(name) = &enum_type.name {
766 let fqn = format!("{}.{}", parent_fqn, name);
767 let rust_path = format!("{}::{}", parent_mod, name);
768 type_map.insert(fqn.clone(), rust_path);
769 package_of.insert(fqn, package.to_string());
770 }
771 }
772}
773
774fn register_enum_closedness(
776 map: &mut HashMap<String, bool>,
777 fqn: &str,
778 parent_features: &ResolvedFeatures,
779 enum_desc: &EnumDescriptorProto,
780) {
781 let resolved = features::resolve_child(parent_features, features::enum_features(enum_desc));
782 let closed = resolved.enum_type == features::EnumType::Closed;
783 map.insert(fqn.to_string(), closed);
784}
785
786fn register_nested_enum_closedness(
789 map: &mut HashMap<String, bool>,
790 parent_fqn: &str,
791 parent_features: &ResolvedFeatures,
792 msg: &DescriptorProto,
793) {
794 let msg_features = features::resolve_child(parent_features, features::message_features(msg));
795 for enum_type in &msg.enum_type {
796 if let Some(name) = &enum_type.name {
797 let fqn = format!("{}.{}", parent_fqn, name);
798 register_enum_closedness(map, &fqn, &msg_features, enum_type);
799 }
800 }
801 for nested in &msg.nested_type {
802 if let Some(name) = &nested.name {
803 let fqn = format!("{}.{}", parent_fqn, name);
804 register_nested_enum_closedness(map, &fqn, &msg_features, nested);
805 }
806 }
807}
808
809#[cfg(test)]
810mod tests {
811 use super::*;
812 use crate::generated::descriptor::{DescriptorProto, EnumDescriptorProto, FileDescriptorProto};
813
814 fn make_file(
815 name: &str,
816 package: &str,
817 messages: Vec<DescriptorProto>,
818 enums: Vec<EnumDescriptorProto>,
819 ) -> FileDescriptorProto {
820 FileDescriptorProto {
821 name: Some(name.to_string()),
822 package: if package.is_empty() {
823 None
824 } else {
825 Some(package.to_string())
826 },
827 message_type: messages,
828 enum_type: enums,
829 ..Default::default()
830 }
831 }
832
833 fn msg(name: &str) -> DescriptorProto {
834 DescriptorProto {
835 name: Some(name.to_string()),
836 ..Default::default()
837 }
838 }
839
840 fn msg_with_nested(name: &str, nested: Vec<DescriptorProto>) -> DescriptorProto {
841 DescriptorProto {
842 name: Some(name.to_string()),
843 nested_type: nested,
844 ..Default::default()
845 }
846 }
847
848 fn msg_with_nested_and_enums(
849 name: &str,
850 nested: Vec<DescriptorProto>,
851 enums: Vec<EnumDescriptorProto>,
852 ) -> DescriptorProto {
853 DescriptorProto {
854 name: Some(name.to_string()),
855 nested_type: nested,
856 enum_type: enums,
857 ..Default::default()
858 }
859 }
860
861 fn enum_desc(name: &str) -> EnumDescriptorProto {
862 EnumDescriptorProto {
863 name: Some(name.to_string()),
864 ..Default::default()
865 }
866 }
867
868 fn enum_with_closed_feature(name: &str) -> EnumDescriptorProto {
869 use crate::generated::descriptor::{feature_set, EnumOptions, FeatureSet};
870 EnumDescriptorProto {
871 name: Some(name.to_string()),
872 options: buffa::MessageField::some(EnumOptions {
873 features: buffa::MessageField::some(FeatureSet {
874 enum_type: Some(feature_set::EnumType::CLOSED),
875 ..Default::default()
876 }),
877 ..Default::default()
878 }),
879 ..Default::default()
880 }
881 }
882
883 fn editions_file(
884 name: &str,
885 package: &str,
886 messages: Vec<DescriptorProto>,
887 enums: Vec<EnumDescriptorProto>,
888 ) -> FileDescriptorProto {
889 use crate::generated::descriptor::Edition;
890 FileDescriptorProto {
891 name: Some(name.to_string()),
892 package: Some(package.to_string()),
893 syntax: Some("editions".to_string()),
894 edition: Some(Edition::EDITION_2023),
895 message_type: messages,
896 enum_type: enums,
897 ..Default::default()
898 }
899 }
900
901 #[test]
904 fn test_message_with_package() {
905 let files = [make_file(
906 "test.proto",
907 "my.package",
908 vec![msg("Foo")],
909 vec![],
910 )];
911 let config = CodeGenConfig::default();
912 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
913 assert_eq!(ctx.rust_type(".my.package.Foo"), Some("my::package::Foo"));
914 }
915
916 #[test]
917 fn test_message_no_package() {
918 let files = [make_file("test.proto", "", vec![msg("Bar")], vec![])];
919 let config = CodeGenConfig::default();
920 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
921 assert_eq!(ctx.rust_type(".Bar"), Some("Bar"));
922 }
923
924 #[test]
925 fn test_nested_message_uses_module_path() {
926 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
927 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
928 let config = CodeGenConfig::default();
929 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
930 assert_eq!(ctx.rust_type(".pkg.Outer"), Some("pkg::Outer"));
931 assert_eq!(ctx.rust_type(".pkg.Outer.Inner"), Some("pkg::outer::Inner"));
933 }
934
935 #[test]
936 fn test_nested_message_no_package() {
937 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
938 let files = [make_file("test.proto", "", vec![outer], vec![])];
939 let config = CodeGenConfig::default();
940 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
941 assert_eq!(ctx.rust_type(".Outer"), Some("Outer"));
942 assert_eq!(ctx.rust_type(".Outer.Inner"), Some("outer::Inner"));
943 }
944
945 #[test]
946 fn test_deeply_nested_message() {
947 let deep = msg_with_nested("A", vec![msg_with_nested("B", vec![msg("C")])]);
948 let files = [make_file("test.proto", "pkg", vec![deep], vec![])];
949 let config = CodeGenConfig::default();
950 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
951 assert_eq!(ctx.rust_type(".pkg.A"), Some("pkg::A"));
952 assert_eq!(ctx.rust_type(".pkg.A.B"), Some("pkg::a::B"));
953 assert_eq!(ctx.rust_type(".pkg.A.B.C"), Some("pkg::a::b::C"));
954 }
955
956 #[test]
957 fn test_nested_enum_uses_module_path() {
958 let outer = msg_with_nested_and_enums("Outer", vec![], vec![enum_desc("Status")]);
959 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
960 let config = CodeGenConfig::default();
961 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
962 assert_eq!(
963 ctx.rust_type(".pkg.Outer.Status"),
964 Some("pkg::outer::Status")
965 );
966 }
967
968 #[test]
969 fn test_top_level_enum() {
970 let files = [make_file(
971 "test.proto",
972 "pkg",
973 vec![],
974 vec![enum_desc("Status")],
975 )];
976 let config = CodeGenConfig::default();
977 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
978 assert_eq!(ctx.rust_type(".pkg.Status"), Some("pkg::Status"));
979 }
980
981 #[test]
982 fn test_same_named_nested_types_in_different_parents_are_distinct() {
983 let outer1 = msg_with_nested("Outer1", vec![msg("Inner")]);
984 let outer2 = msg_with_nested("Outer2", vec![msg("Inner")]);
985 let files = [make_file("a.proto", "pkg", vec![outer1, outer2], vec![])];
986 let config = CodeGenConfig::default();
987 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
988 assert_eq!(
990 ctx.rust_type(".pkg.Outer1.Inner"),
991 Some("pkg::outer1::Inner")
992 );
993 assert_eq!(
994 ctx.rust_type(".pkg.Outer2.Inner"),
995 Some("pkg::outer2::Inner")
996 );
997 assert_ne!(
998 ctx.rust_type(".pkg.Outer1.Inner"),
999 ctx.rust_type(".pkg.Outer2.Inner")
1000 );
1001 }
1002
1003 #[test]
1004 fn test_multiple_files() {
1005 let files = [
1006 make_file("a.proto", "ns.a", vec![msg("MsgA")], vec![]),
1007 make_file("b.proto", "ns.b", vec![msg("MsgB")], vec![]),
1008 ];
1009 let config = CodeGenConfig::default();
1010 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1011 assert_eq!(ctx.rust_type(".ns.a.MsgA"), Some("ns::a::MsgA"));
1012 assert_eq!(ctx.rust_type(".ns.b.MsgB"), Some("ns::b::MsgB"));
1013 }
1014
1015 #[test]
1016 fn test_keyword_package_segment_in_type_map() {
1017 let files = [make_file(
1020 "latlng.proto",
1021 "google.type",
1022 vec![msg("LatLng")],
1023 vec![],
1024 )];
1025 let config = CodeGenConfig::default();
1026 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1027 assert_eq!(
1028 ctx.rust_type(".google.type.LatLng"),
1029 Some("google::type::LatLng")
1030 );
1031 }
1032
1033 #[test]
1034 fn test_keyword_package_relative_same_package() {
1035 let files = [make_file(
1036 "latlng.proto",
1037 "google.type",
1038 vec![msg("LatLng"), msg("Expr")],
1039 vec![],
1040 )];
1041 let config = CodeGenConfig::default();
1042 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1043 assert_eq!(
1045 ctx.rust_type_relative(".google.type.LatLng", "google.type", 0),
1046 Some("LatLng".into())
1047 );
1048 }
1049
1050 #[test]
1051 fn test_keyword_package_cross_package() {
1052 let files = [
1053 make_file("latlng.proto", "google.type", vec![msg("LatLng")], vec![]),
1054 make_file("svc.proto", "google.cloud", vec![msg("Service")], vec![]),
1055 ];
1056 let config = CodeGenConfig::default();
1057 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1058 assert_eq!(
1061 ctx.rust_type_relative(".google.type.LatLng", "google.cloud", 0),
1062 Some("super::type::LatLng".into())
1063 );
1064 }
1065
1066 #[test]
1067 fn test_keyword_nested_message_module() {
1068 let outer = msg_with_nested("Type", vec![msg("Inner")]);
1070 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
1071 let config = CodeGenConfig::default();
1072 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1073 assert_eq!(ctx.rust_type(".pkg.Type"), Some("pkg::Type"));
1074 assert_eq!(ctx.rust_type(".pkg.Type.Inner"), Some("pkg::type::Inner"));
1075 }
1076
1077 #[test]
1078 fn test_unknown_type_returns_none() {
1079 let files = [make_file("test.proto", "pkg", vec![msg("Foo")], vec![])];
1080 let config = CodeGenConfig::default();
1081 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1082 assert_eq!(ctx.rust_type(".pkg.Unknown"), None);
1083 }
1084
1085 #[test]
1088 fn test_relative_same_package_top_level() {
1089 let files = [make_file("a.proto", "pkg", vec![msg("Foo")], vec![])];
1090 let config = CodeGenConfig::default();
1091 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1092 assert_eq!(
1094 ctx.rust_type_relative(".pkg.Foo", "pkg", 0),
1095 Some("Foo".into())
1096 );
1097 }
1098
1099 #[test]
1100 fn test_relative_cross_package() {
1101 let files = [
1102 make_file("a.proto", "pkg_a", vec![msg("Foo")], vec![]),
1103 make_file("b.proto", "pkg_b", vec![msg("Bar")], vec![]),
1104 ];
1105 let config = CodeGenConfig::default();
1106 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1107 assert_eq!(
1109 ctx.rust_type_relative(".pkg_a.Foo", "pkg_b", 0),
1110 Some("super::pkg_a::Foo".into())
1111 );
1112 }
1113
1114 #[test]
1115 fn test_relative_no_package() {
1116 let files = [make_file("a.proto", "", vec![msg("Foo")], vec![])];
1117 let config = CodeGenConfig::default();
1118 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1119 assert_eq!(ctx.rust_type_relative(".Foo", "", 0), Some("Foo".into()));
1120 }
1121
1122 #[test]
1123 fn test_relative_unknown_returns_none() {
1124 let files = [make_file("a.proto", "pkg", vec![msg("Foo")], vec![])];
1125 let config = CodeGenConfig::default();
1126 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1127 assert_eq!(ctx.rust_type_relative(".pkg.Unknown", "pkg", 0), None);
1128 }
1129
1130 #[test]
1131 fn test_relative_dotted_package() {
1132 let files = [make_file("a.proto", "my.pkg", vec![msg("Foo")], vec![])];
1133 let config = CodeGenConfig::default();
1134 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1135 assert_eq!(
1136 ctx.rust_type_relative(".my.pkg.Foo", "my.pkg", 0),
1137 Some("Foo".into())
1138 );
1139 }
1140
1141 #[test]
1142 fn test_relative_cross_dotted_packages() {
1143 let files = [
1144 make_file(
1145 "timestamp.proto",
1146 "google.protobuf",
1147 vec![msg("Timestamp")],
1148 vec![],
1149 ),
1150 make_file(
1151 "test.proto",
1152 "protobuf_test_messages.proto3",
1153 vec![msg("TestAllTypesProto3")],
1154 vec![],
1155 ),
1156 ];
1157 let config = CodeGenConfig::default();
1158 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1159
1160 assert_eq!(
1162 ctx.rust_type_relative(
1163 ".google.protobuf.Timestamp",
1164 "protobuf_test_messages.proto3",
1165 0,
1166 ),
1167 Some("super::super::google::protobuf::Timestamp".into())
1168 );
1169 }
1170
1171 #[test]
1172 fn test_relative_nested_type_from_same_package() {
1173 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
1175 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
1176 let config = CodeGenConfig::default();
1177 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1178
1179 assert_eq!(
1181 ctx.rust_type_relative(".pkg.Outer.Inner", "pkg", 0),
1182 Some("outer::Inner".into())
1183 );
1184 }
1185
1186 #[test]
1187 fn test_relative_shared_prefix_not_confused() {
1188 let files = [
1189 make_file("ab.proto", "a.b", vec![msg("Msg1")], vec![]),
1190 make_file("abc.proto", "a.bc", vec![msg("Msg2")], vec![]),
1191 ];
1192 let config = CodeGenConfig::default();
1193 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1194
1195 assert_eq!(
1197 ctx.rust_type_relative(".a.b.Msg1", "a.bc", 0),
1198 Some("super::b::Msg1".into())
1199 );
1200 assert_eq!(
1202 ctx.rust_type_relative(".a.bc.Msg2", "a.b", 0),
1203 Some("super::bc::Msg2".into())
1204 );
1205 }
1206
1207 #[test]
1210 fn test_relative_cross_package_nesting_1() {
1211 let outer = msg_with_nested_and_enums("Business", vec![], vec![enum_desc("Status")]);
1215 let files = [
1216 make_file("admin.proto", "a.b.admin.v1", vec![msg("Svc")], vec![]),
1217 make_file("biz.proto", "a.b.v1", vec![outer], vec![]),
1218 ];
1219 let config = CodeGenConfig::default();
1220 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1221
1222 assert_eq!(
1224 ctx.rust_type_relative(".a.b.v1.Business.Status", "a.b.admin.v1", 0),
1225 Some("super::super::v1::business::Status".into())
1226 );
1227 assert_eq!(
1229 ctx.rust_type_relative(".a.b.v1.Business.Status", "a.b.admin.v1", 1),
1230 Some("super::super::super::v1::business::Status".into())
1231 );
1232 }
1233
1234 #[test]
1235 fn test_relative_same_package_nesting_1() {
1236 let files = [make_file(
1238 "test.proto",
1239 "pkg",
1240 vec![msg("Foo"), msg("Bar")],
1241 vec![],
1242 )];
1243 let config = CodeGenConfig::default();
1244 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1245
1246 assert_eq!(
1248 ctx.rust_type_relative(".pkg.Foo", "pkg", 0),
1249 Some("Foo".into())
1250 );
1251 assert_eq!(
1253 ctx.rust_type_relative(".pkg.Foo", "pkg", 1),
1254 Some("super::Foo".into())
1255 );
1256 assert_eq!(
1258 ctx.rust_type_relative(".pkg.Foo", "pkg", 2),
1259 Some("super::super::Foo".into())
1260 );
1261 }
1262
1263 #[test]
1266 fn test_resolve_file_extern_exact_match_only() {
1267 let mappings = [(
1268 "google/protobuf/descriptor.proto".to_string(),
1269 "::buffa_descriptor::generated::descriptor".to_string(),
1270 )];
1271 assert_eq!(
1273 resolve_file_extern("google/protobuf/descriptor.proto", &mappings),
1274 Some("::buffa_descriptor::generated::descriptor"),
1275 );
1276 assert_eq!(
1279 resolve_file_extern("google/protobuf/timestamp.proto", &mappings),
1280 None,
1281 );
1282 assert_eq!(
1284 resolve_file_extern("vendor/google/protobuf/descriptor.proto", &mappings),
1285 None,
1286 );
1287 }
1288
1289 #[test]
1290 fn test_resolve_extern_prefix_exact_match() {
1291 let result = resolve_extern_prefix(
1292 "my.common",
1293 &[(".my.common".into(), "::common_protos".into())],
1294 );
1295 assert_eq!(result, Some("::common_protos".into()));
1296 }
1297
1298 #[test]
1299 fn test_resolve_extern_prefix_sub_package() {
1300 let result = resolve_extern_prefix(
1301 "my.common.sub",
1302 &[(".my.common".into(), "::common_protos".into())],
1303 );
1304 assert_eq!(result, Some("::common_protos::sub".into()));
1305 }
1306
1307 #[test]
1308 fn test_resolve_extern_prefix_no_match() {
1309 let result = resolve_extern_prefix(
1310 "other.pkg",
1311 &[(".my.common".into(), "::common_protos".into())],
1312 );
1313 assert_eq!(result, None);
1314 }
1315
1316 #[test]
1317 fn test_resolve_extern_prefix_partial_name_no_match() {
1318 let result = resolve_extern_prefix(
1320 "my.commonext",
1321 &[(".my.common".into(), "::common_protos".into())],
1322 );
1323 assert_eq!(result, None);
1324 }
1325
1326 #[test]
1327 fn test_resolve_extern_prefix_longest_match_wins() {
1328 let result = resolve_extern_prefix(
1330 "my.common.sub",
1331 &[
1332 (".my".into(), "::crate_a".into()),
1333 (".my.common".into(), "::crate_b".into()),
1334 ],
1335 );
1336 assert_eq!(result, Some("::crate_b::sub".into()));
1337 }
1338
1339 #[test]
1340 fn test_resolve_extern_prefix_catchall() {
1341 let result = resolve_extern_prefix("greet.v1", &[(".".into(), "crate::proto".into())]);
1342 assert_eq!(result, Some("crate::proto::greet::v1".into()));
1343 }
1344
1345 #[test]
1346 fn test_resolve_extern_prefix_catchall_empty_pkg() {
1347 let result = resolve_extern_prefix("", &[(".".into(), "crate::proto".into())]);
1350 assert_eq!(result, Some("crate::proto".into()));
1351 }
1352
1353 #[test]
1354 fn test_resolve_extern_prefix_catchall_longest_wins() {
1355 let result = resolve_extern_prefix(
1358 "google.protobuf",
1359 &[
1360 (".".into(), "crate::proto".into()),
1361 (
1362 ".google.protobuf".into(),
1363 "::buffa_types::google::protobuf".into(),
1364 ),
1365 ],
1366 );
1367 assert_eq!(result, Some("::buffa_types::google::protobuf".into()));
1368 }
1369
1370 #[test]
1371 fn test_resolve_extern_prefix_catchall_keyword_package() {
1372 let result = resolve_extern_prefix("google.type", &[(".".into(), "crate::proto".into())]);
1375 assert_eq!(result, Some("crate::proto::google::type".into()));
1376 }
1377
1378 #[test]
1381 fn test_split_extern_top_level() {
1382 let outer = msg_with_nested("Value", vec![msg("Inner")]);
1383 let files = [make_file(
1384 "struct.proto",
1385 "google.protobuf",
1386 vec![outer],
1387 vec![],
1388 )];
1389 let config = CodeGenConfig::default();
1390 let extern_paths = vec![(
1391 ".google.protobuf".into(),
1392 "::buffa_types::google::protobuf".into(),
1393 )];
1394 let ctx = CodeGenContext::new(&files, &config, &extern_paths);
1395
1396 let split = ctx
1397 .rust_type_relative_split(".google.protobuf.Value", "my.pkg", 3)
1398 .expect("type resolves");
1399 assert!(split.is_extern);
1400 assert_eq!(split.to_package, "::buffa_types::google::protobuf");
1402 assert_eq!(split.within_package, "Value");
1403 }
1404
1405 #[test]
1406 fn test_split_extern_nested_type() {
1407 let outer = msg_with_nested("Value", vec![msg("Inner")]);
1412 let files = [make_file(
1413 "struct.proto",
1414 "google.protobuf",
1415 vec![outer],
1416 vec![],
1417 )];
1418 let config = CodeGenConfig::default();
1419 let extern_paths = vec![(
1420 ".google.protobuf".into(),
1421 "::buffa_types::google::protobuf".into(),
1422 )];
1423 let ctx = CodeGenContext::new(&files, &config, &extern_paths);
1424
1425 let split = ctx
1426 .rust_type_relative_split(".google.protobuf.Value.Inner", "my.pkg", 0)
1427 .expect("nested type resolves");
1428 assert!(split.is_extern);
1429 assert_eq!(split.to_package, "::buffa_types::google::protobuf");
1430 assert_eq!(split.within_package, "value::Inner");
1431 }
1432
1433 #[test]
1434 fn test_extern_path_top_level_message() {
1435 let files = [make_file(
1436 "common.proto",
1437 "my.common",
1438 vec![msg("SharedMsg")],
1439 vec![],
1440 )];
1441 let config = CodeGenConfig {
1442 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
1443 ..Default::default()
1444 };
1445 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1446 assert_eq!(
1447 ctx.rust_type(".my.common.SharedMsg"),
1448 Some("::common_protos::SharedMsg")
1449 );
1450 }
1451
1452 #[test]
1453 fn test_extern_path_nested_message() {
1454 let files = [make_file(
1455 "common.proto",
1456 "my.common",
1457 vec![msg_with_nested("Outer", vec![msg("Inner")])],
1458 vec![],
1459 )];
1460 let config = CodeGenConfig {
1461 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
1462 ..Default::default()
1463 };
1464 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1465 assert_eq!(
1466 ctx.rust_type(".my.common.Outer"),
1467 Some("::common_protos::Outer")
1468 );
1469 assert_eq!(
1470 ctx.rust_type(".my.common.Outer.Inner"),
1471 Some("::common_protos::outer::Inner")
1472 );
1473 }
1474
1475 #[test]
1476 fn test_extern_path_enum() {
1477 let files = [make_file(
1478 "common.proto",
1479 "my.common",
1480 vec![],
1481 vec![enum_desc("Status")],
1482 )];
1483 let config = CodeGenConfig {
1484 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
1485 ..Default::default()
1486 };
1487 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1488 assert_eq!(
1489 ctx.rust_type(".my.common.Status"),
1490 Some("::common_protos::Status")
1491 );
1492 }
1493
1494 #[test]
1495 fn test_extern_path_does_not_affect_other_packages() {
1496 let files = [
1497 make_file("common.proto", "my.common", vec![msg("SharedMsg")], vec![]),
1498 make_file(
1499 "service.proto",
1500 "my.service",
1501 vec![msg("MyService")],
1502 vec![],
1503 ),
1504 ];
1505 let config = CodeGenConfig {
1506 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
1507 ..Default::default()
1508 };
1509 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1510 assert_eq!(
1512 ctx.rust_type(".my.common.SharedMsg"),
1513 Some("::common_protos::SharedMsg")
1514 );
1515 assert_eq!(
1517 ctx.rust_type(".my.service.MyService"),
1518 Some("my::service::MyService")
1519 );
1520 }
1521
1522 #[test]
1523 fn test_extern_path_relative_returns_absolute() {
1524 let files = [
1527 make_file("common.proto", "my.common", vec![msg("SharedMsg")], vec![]),
1528 make_file(
1529 "service.proto",
1530 "my.service",
1531 vec![msg("MyService")],
1532 vec![],
1533 ),
1534 ];
1535 let config = CodeGenConfig {
1536 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
1537 ..Default::default()
1538 };
1539 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1540 assert_eq!(
1542 ctx.rust_type_relative(".my.common.SharedMsg", "my.service", 0),
1543 Some("::common_protos::SharedMsg".into())
1544 );
1545 }
1546
1547 #[test]
1550 fn test_is_enum_closed_proto3_default_open() {
1551 let files = [make_file("a.proto", "p", vec![], vec![enum_desc("E")])];
1552 let config = CodeGenConfig::default();
1553 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1554 assert_eq!(ctx.is_enum_closed(".p.E"), Some(true));
1558 }
1559
1560 #[test]
1561 fn test_is_enum_closed_editions_default_open() {
1562 let files = [editions_file("a.proto", "p", vec![], vec![enum_desc("E")])];
1563 let config = CodeGenConfig::default();
1564 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1565 assert_eq!(ctx.is_enum_closed(".p.E"), Some(false));
1567 }
1568
1569 #[test]
1570 fn test_is_enum_closed_per_enum_override() {
1571 let files = [editions_file(
1574 "a.proto",
1575 "p",
1576 vec![],
1577 vec![enum_desc("Open"), enum_with_closed_feature("Closed")],
1578 )];
1579 let config = CodeGenConfig::default();
1580 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1581 assert_eq!(ctx.is_enum_closed(".p.Open"), Some(false));
1582 assert_eq!(ctx.is_enum_closed(".p.Closed"), Some(true));
1583 }
1584
1585 #[test]
1586 fn test_is_enum_closed_nested_per_enum_override() {
1587 let files = [editions_file(
1589 "a.proto",
1590 "p",
1591 vec![msg_with_nested_and_enums(
1592 "M",
1593 vec![],
1594 vec![enum_with_closed_feature("Inner")],
1595 )],
1596 vec![],
1597 )];
1598 let config = CodeGenConfig::default();
1599 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1600 assert_eq!(ctx.is_enum_closed(".p.M.Inner"), Some(true));
1601 }
1602
1603 #[test]
1604 fn test_is_enum_closed_unknown_enum_returns_none() {
1605 let files = [editions_file("a.proto", "p", vec![], vec![])];
1606 let config = CodeGenConfig::default();
1607 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1608 assert_eq!(ctx.is_enum_closed(".other.Unknown"), None);
1610 }
1611
1612 #[test]
1613 fn test_for_generate_auto_injects_wkt_mapping() {
1614 let ts_msg = DescriptorProto {
1617 name: Some("Timestamp".into()),
1618 ..Default::default()
1619 };
1620 let files = [FileDescriptorProto {
1621 name: Some("google/protobuf/timestamp.proto".into()),
1622 package: Some("google.protobuf".into()),
1623 syntax: Some("proto3".into()),
1624 message_type: vec![ts_msg],
1625 ..Default::default()
1626 }];
1627 let config = CodeGenConfig::default();
1628 let ctx = CodeGenContext::for_generate(&files, &["other.proto".into()], &config);
1630 assert_eq!(
1631 ctx.rust_type(".google.protobuf.Timestamp"),
1632 Some("::buffa_types::google::protobuf::Timestamp"),
1633 "WKT auto-mapping must be applied via for_generate"
1634 );
1635 }
1636
1637 #[test]
1638 fn test_for_generate_suppresses_wkt_when_generating_wkt() {
1639 let ts_msg = DescriptorProto {
1642 name: Some("Timestamp".into()),
1643 ..Default::default()
1644 };
1645 let files = [FileDescriptorProto {
1646 name: Some("google/protobuf/timestamp.proto".into()),
1647 package: Some("google.protobuf".into()),
1648 syntax: Some("proto3".into()),
1649 message_type: vec![ts_msg],
1650 ..Default::default()
1651 }];
1652 let config = CodeGenConfig::default();
1653 let ctx = CodeGenContext::for_generate(
1654 &files,
1655 &["google/protobuf/timestamp.proto".into()],
1656 &config,
1657 );
1658 assert_eq!(
1660 ctx.rust_type(".google.protobuf.Timestamp"),
1661 Some("google::protobuf::Timestamp")
1662 );
1663 }
1664
1665 #[test]
1668 fn test_matching_attributes_catchall() {
1669 let attrs = vec![(".".into(), "#[derive(Foo)]".into())];
1671 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
1672 assert!(result.to_string().contains("derive"));
1673 }
1674
1675 #[test]
1676 fn test_matching_attributes_exact_match() {
1677 let attrs = vec![(".my.pkg.MyMessage".into(), "#[derive(Bar)]".into())];
1678 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
1679 assert!(result.to_string().contains("derive"));
1680 }
1681
1682 #[test]
1683 fn test_matching_attributes_package_prefix() {
1684 let attrs = vec![(".my.pkg".into(), "#[derive(Baz)]".into())];
1685 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
1686 assert!(result.to_string().contains("derive"));
1687 }
1688
1689 #[test]
1690 fn test_matching_attributes_no_partial_segment_match() {
1691 let attrs = vec![(".my.pk".into(), "#[derive(Bad)]".into())];
1693 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
1694 assert!(result.is_empty());
1695 }
1696
1697 #[test]
1698 fn test_matching_attributes_no_match() {
1699 let attrs = vec![(".other.pkg".into(), "#[derive(Nope)]".into())];
1700 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
1701 assert!(result.is_empty());
1702 }
1703
1704 #[test]
1705 fn test_matching_attributes_multiple_accumulate() {
1706 let attrs = vec![
1708 (".".into(), "#[derive(A)]".into()),
1709 (".my.pkg".into(), "#[derive(B)]".into()),
1710 ];
1711 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
1712 let s = result.to_string();
1713 assert!(s.contains("A") && s.contains("B"));
1714 }
1715
1716 #[test]
1717 fn test_matching_attributes_invalid_attr_errors() {
1718 let attrs = vec![(".".into(), "not valid {{{{".into())];
1721 let err = CodeGenContext::matching_attributes(&attrs, "my.pkg.Msg").unwrap_err();
1722 assert!(matches!(
1723 err,
1724 crate::CodeGenError::InvalidCustomAttribute { .. }
1725 ));
1726 }
1727
1728 #[test]
1729 fn test_matches_proto_prefix_catchall() {
1730 assert!(matches_proto_prefix(".", ".anything.here"));
1731 assert!(matches_proto_prefix(".", "."));
1732 }
1733
1734 #[test]
1735 fn test_matches_proto_prefix_segment_boundary() {
1736 assert!(!matches_proto_prefix(".my.pk", ".my.pkg.Msg"));
1738 assert!(matches_proto_prefix(".my.pkg", ".my.pkg.Msg"));
1740 }
1741}