1use std::collections::{HashMap, HashSet};
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 nested_module_names: HashMap<String, String>,
93 warnings: std::cell::RefCell<Vec<crate::CodeGenWarning>>,
105}
106
107fn child_package_segments(package: &str, all_packages: &HashSet<String>) -> HashSet<String> {
114 let prefix = if package.is_empty() {
115 String::new()
116 } else {
117 format!("{package}.")
118 };
119 all_packages
120 .iter()
121 .filter_map(|p| {
122 let rest = if package.is_empty() {
123 Some(p.as_str())
124 } else {
125 p.strip_prefix(&prefix)
126 };
127 rest.filter(|r| !r.is_empty())
128 .map(|r| r.split('.').next().unwrap_or(r).to_string())
129 })
130 .collect()
131}
132
133fn deconflict_package_modules(message_names: &[String], children: &HashSet<String>) -> Vec<String> {
153 let bases: Vec<String> = message_names.iter().map(|n| to_snake_case(n)).collect();
154 let mut taken: HashSet<String> = children.clone();
157 taken.insert(SENTINEL_MOD.to_string());
158 taken.extend(bases.iter().cloned());
159
160 let mut out = bases.clone();
165 let mut order: Vec<usize> = (0..bases.len()).collect();
166 order.sort_by(|&a, &b| bases[a].cmp(&bases[b]));
167 for i in order {
168 if !children.contains(&bases[i]) {
169 continue;
170 }
171 let mut candidate = format!("{}_", bases[i]);
172 while taken.contains(&candidate) {
173 candidate.push('_');
174 }
175 taken.insert(candidate.clone());
176 out[i] = candidate;
177 }
178 out
179}
180
181impl<'a> CodeGenContext<'a> {
182 pub fn new(
194 files: &'a [FileDescriptorProto],
195 config: &'a CodeGenConfig,
196 effective_extern_paths: &[(String, String)],
197 ) -> Self {
198 Self::with_extern_resolution(files, config, effective_extern_paths, &[])
199 }
200
201 pub(crate) fn with_extern_resolution(
224 files: &'a [FileDescriptorProto],
225 config: &'a CodeGenConfig,
226 effective_extern_paths: &[(String, String)],
227 file_extern_paths: &[(String, String)],
228 ) -> Self {
229 let mut type_map = HashMap::new();
230 let mut package_of = HashMap::new();
231 let mut enum_closedness = HashMap::new();
232 let mut comment_map = HashMap::new();
233 let mut nested_module_names = HashMap::new();
234
235 let mut all_packages: HashSet<String> = HashSet::new();
241 let mut pkg_message_names: HashMap<String, Vec<String>> = HashMap::new();
242 for file in files {
243 let package = file.package.as_deref().unwrap_or("");
244 let is_extern = file
245 .name
246 .as_deref()
247 .and_then(|n| resolve_file_extern(n, file_extern_paths))
248 .is_some()
249 || resolve_extern_prefix(package, effective_extern_paths).is_some();
250 if is_extern {
251 continue;
252 }
253 all_packages.insert(package.to_string());
254 for msg in &file.message_type {
255 if let Some(name) = &msg.name {
256 let fqn = if package.is_empty() {
260 format!(".{name}")
261 } else {
262 format!(".{package}.{name}")
263 };
264 if effective_extern_paths
265 .iter()
266 .any(|(proto, _)| proto == &fqn)
267 {
268 continue;
269 }
270 pkg_message_names
271 .entry(package.to_string())
272 .or_default()
273 .push(name.clone());
274 }
275 }
276 }
277
278 for (package, names) in &pkg_message_names {
283 let children = child_package_segments(package, &all_packages);
284 let modules = deconflict_package_modules(names, &children);
285 for (name, module) in names.iter().zip(modules) {
286 let fqn = if package.is_empty() {
287 format!(".{name}")
288 } else {
289 format!(".{package}.{name}")
290 };
291 nested_module_names.insert(fqn, module);
292 }
293 }
294
295 for file in files {
296 comment_map.extend(crate::comments::fqn_comments(file));
297 let package = file.package.as_deref().unwrap_or("");
298 let file_features = features::for_file(file);
299 let proto_prefix = if package.is_empty() {
300 String::from(".")
301 } else {
302 format!(".{}.", package)
303 };
304
305 let file_root = file
315 .name
316 .as_deref()
317 .and_then(|n| resolve_file_extern(n, file_extern_paths));
318 let local_module = package.replace('.', "::");
319
320 for msg in &file.message_type {
322 if let Some(name) = &msg.name {
323 let fqn = format!("{}{}", proto_prefix, name);
324 let (rust_path, is_extern) = resolve_type_path(
325 &fqn,
326 name,
327 file_root,
328 &local_module,
329 effective_extern_paths,
330 );
331
332 let parent_mod = if is_extern {
340 match rust_path.rsplit_once("::") {
341 Some((parent, _)) => format!("{parent}::{}", to_snake_case(name)),
342 None => to_snake_case(name),
343 }
344 } else {
345 let snake = nested_module_names
346 .get(&fqn)
347 .cloned()
348 .unwrap_or_else(|| to_snake_case(name));
349 join_mod(&local_module, &snake)
350 };
351
352 type_map.insert(fqn.clone(), rust_path);
353 package_of.insert(fqn.clone(), package.to_string());
354 register_nested_types(
355 &mut type_map,
356 &mut package_of,
357 package,
358 &fqn,
359 &parent_mod,
360 msg,
361 effective_extern_paths,
362 );
363 register_nested_enum_closedness(
364 &mut enum_closedness,
365 &fqn,
366 &file_features,
367 msg,
368 );
369 }
370 }
371
372 for enum_type in &file.enum_type {
374 if let Some(name) = &enum_type.name {
375 let fqn = format!("{}{}", proto_prefix, name);
376 let (rust_path, _) = resolve_type_path(
377 &fqn,
378 name,
379 file_root,
380 &local_module,
381 effective_extern_paths,
382 );
383 type_map.insert(fqn.clone(), rust_path);
384 package_of.insert(fqn.clone(), package.to_string());
385 register_enum_closedness(&mut enum_closedness, &fqn, &file_features, enum_type);
386 }
387 }
388 }
389
390 Self {
391 files,
392 config,
393 type_map,
394 package_of,
395 enum_closedness,
396 comment_map,
397 nested_module_names,
398 warnings: std::cell::RefCell::new(Vec::new()),
399 }
400 }
401
402 pub(crate) fn warn(&self, warning: crate::CodeGenWarning) {
404 self.warnings.borrow_mut().push(warning);
405 }
406
407 pub(crate) fn take_warnings(&self) -> Vec<crate::CodeGenWarning> {
413 self.warnings.take()
414 }
415
416 pub fn nested_module_name(&self, package: &str, name: &str) -> String {
425 let fqn = if package.is_empty() {
426 format!(".{name}")
427 } else {
428 format!(".{package}.{name}")
429 };
430 self.nested_module_names
431 .get(&fqn)
432 .cloned()
433 .unwrap_or_else(|| to_snake_case(name))
434 }
435
436 pub fn for_generate(
450 files: &'a [FileDescriptorProto],
451 files_to_generate: &[String],
452 config: &'a CodeGenConfig,
453 ) -> Self {
454 let paths = crate::effective_extern_paths(files, files_to_generate, config);
455 let file_paths = crate::effective_file_extern_paths(files_to_generate, config);
456 Self::with_extern_resolution(files, config, &paths, &file_paths)
457 }
458
459 pub fn rust_type(&self, proto_fqn: &str) -> Option<&str> {
461 self.type_map.get(proto_fqn).map(|s| s.as_str())
462 }
463
464 pub fn comment(&self, fqn: &str) -> Option<&str> {
473 self.comment_map.get(fqn).map(|s| s.as_str())
474 }
475
476 pub fn is_enum_closed(&self, proto_fqn: &str) -> Option<bool> {
483 self.enum_closedness.get(proto_fqn).copied()
484 }
485
486 pub fn rust_type_relative(
501 &self,
502 proto_fqn: &str,
503 current_package: &str,
504 nesting: usize,
505 ) -> Option<String> {
506 let full_path = self.type_map.get(proto_fqn)?;
507
508 if full_path.starts_with("::") || full_path.starts_with("crate::") {
511 return Some(full_path.clone());
512 }
513
514 let target_package = self
515 .package_of
516 .get(proto_fqn)
517 .map(|s| s.as_str())
518 .unwrap_or("");
519
520 let target_rust_module = target_package.replace('.', "::");
523 let type_suffix = if target_rust_module.is_empty() {
524 full_path.as_str()
525 } else {
526 full_path
527 .strip_prefix(&format!("{}::", target_rust_module))
528 .unwrap_or(full_path)
529 };
530
531 if current_package == target_package {
532 if nesting == 0 {
534 return Some(type_suffix.to_string());
535 }
536 let supers = (0..nesting).map(|_| "super").collect::<Vec<_>>().join("::");
537 return Some(format!("{}::{}", supers, type_suffix));
538 }
539
540 let current_parts: Vec<&str> = if current_package.is_empty() {
542 vec![]
543 } else {
544 current_package.split('.').collect()
545 };
546 let target_parts: Vec<&str> = if target_package.is_empty() {
547 vec![]
548 } else {
549 target_package.split('.').collect()
550 };
551
552 let common_len = current_parts
554 .iter()
555 .zip(&target_parts)
556 .take_while(|(a, b)| a == b)
557 .count();
558
559 let up_count = (current_parts.len() - common_len) + nesting;
562
563 let down_parts = &target_parts[common_len..];
565
566 let mut segments: Vec<&str> = vec!["super"; up_count];
567 segments.extend_from_slice(down_parts);
568
569 let mut result = segments.join("::");
571 if !result.is_empty() {
572 result.push_str("::");
573 }
574 result.push_str(type_suffix);
575
576 Some(result)
577 }
578
579 pub fn rust_type_relative_split(
591 &self,
592 proto_fqn: &str,
593 current_package: &str,
594 nesting: usize,
595 ) -> Option<SplitPath> {
596 let full_path = self.type_map.get(proto_fqn)?;
597
598 let target_package = self
599 .package_of
600 .get(proto_fqn)
601 .map(|s| s.as_str())
602 .unwrap_or("");
603
604 let target_rust_module = if full_path.starts_with("::") || full_path.starts_with("crate::")
610 {
611 let fqn_no_dot = proto_fqn.strip_prefix('.').unwrap_or(proto_fqn);
620 let within_proto = if target_package.is_empty() {
621 fqn_no_dot
622 } else {
623 fqn_no_dot
624 .strip_prefix(target_package)
625 .and_then(|s| s.strip_prefix('.'))
626 .unwrap_or(fqn_no_dot)
627 };
628 let within_segs = within_proto.split('.').count();
642 let full_segs: Vec<&str> = full_path.split("::").collect();
643 let cut = full_segs.len().saturating_sub(within_segs);
644 full_segs[..cut].join("::")
645 } else {
646 target_package.replace('.', "::")
647 };
648
649 let type_suffix = if target_rust_module.is_empty() {
650 full_path.as_str()
651 } else {
652 full_path
653 .strip_prefix(&format!("{}::", target_rust_module))
654 .unwrap_or(full_path)
655 };
656
657 if full_path.starts_with("::") || full_path.starts_with("crate::") {
659 return Some(SplitPath {
660 to_package: target_rust_module,
661 within_package: type_suffix.to_string(),
662 is_extern: true,
663 });
664 }
665
666 if current_package == target_package {
667 let to_package = if nesting == 0 {
668 String::new()
669 } else {
670 (0..nesting).map(|_| "super").collect::<Vec<_>>().join("::")
671 };
672 return Some(SplitPath {
673 to_package,
674 within_package: type_suffix.to_string(),
675 is_extern: false,
676 });
677 }
678
679 let current_parts: Vec<&str> = if current_package.is_empty() {
681 vec![]
682 } else {
683 current_package.split('.').collect()
684 };
685 let target_parts: Vec<&str> = if target_package.is_empty() {
686 vec![]
687 } else {
688 target_package.split('.').collect()
689 };
690 let common_len = current_parts
691 .iter()
692 .zip(&target_parts)
693 .take_while(|(a, b)| a == b)
694 .count();
695 let up_count = (current_parts.len() - common_len) + nesting;
696 let down_parts = &target_parts[common_len..];
697
698 let mut segments: Vec<&str> = vec!["super"; up_count];
699 segments.extend_from_slice(down_parts);
700
701 Some(SplitPath {
702 to_package: segments.join("::"),
703 within_package: type_suffix.to_string(),
704 is_extern: false,
705 })
706 }
707
708 pub(crate) fn matching_attributes(
721 attrs: &[(String, String)],
722 fqn: &str,
723 ) -> Result<proc_macro2::TokenStream, crate::CodeGenError> {
724 if attrs.is_empty() {
725 return Ok(proc_macro2::TokenStream::new());
726 }
727 let fqn_dotted = format!(".{fqn}");
728 let mut tokens = proc_macro2::TokenStream::new();
729 for (prefix, attr_str) in attrs {
730 if matches_proto_prefix(prefix, &fqn_dotted) {
731 let parsed =
732 syn::parse_str::<proc_macro2::TokenStream>(attr_str).map_err(|err| {
733 crate::CodeGenError::InvalidCustomAttribute {
734 path: prefix.clone(),
735 attribute: attr_str.clone(),
736 detail: err.to_string(),
737 }
738 })?;
739 tokens.extend(parsed);
740 }
741 }
742 Ok(tokens)
743 }
744
745 pub fn use_bytes_type(&self, field_fqn: &str) -> bool {
753 self.config
754 .bytes_fields
755 .iter()
756 .any(|prefix| matches_proto_prefix(prefix, field_fqn))
757 }
758
759 pub fn string_repr(&self, field_fqn: &str) -> crate::StringRepr {
769 self.config
770 .string_fields
771 .iter()
772 .rev()
773 .find(|(prefix, _)| matches_proto_prefix(prefix, field_fqn))
774 .map_or(crate::StringRepr::default(), |(_, repr)| *repr)
775 }
776}
777
778#[derive(Clone, Copy)]
785pub(crate) struct MessageScope<'a> {
786 pub ctx: &'a CodeGenContext<'a>,
788 pub current_package: &'a str,
790 pub proto_fqn: &'a str,
793 pub features: &'a ResolvedFeatures,
795 pub nesting: usize,
799}
800
801impl<'a> MessageScope<'a> {
802 pub fn nested(&self, proto_fqn: &'a str, features: &'a ResolvedFeatures) -> MessageScope<'a> {
804 MessageScope {
805 ctx: self.ctx,
806 current_package: self.current_package,
807 proto_fqn,
808 features,
809 nesting: self.nesting + 1,
810 }
811 }
812}
813
814#[derive(Debug, Clone, Copy, PartialEq, Eq)]
819pub(crate) enum AncillaryKind {
820 Oneof,
822 View,
824 ViewOneof,
826}
827
828impl AncillaryKind {
829 fn path_segments(self) -> &'static [&'static str] {
830 match self {
831 Self::Oneof => &["oneof"],
832 Self::View => &["view"],
833 Self::ViewOneof => &["view", "oneof"],
834 }
835 }
836}
837
838pub(crate) fn ancillary_prefix(
853 kind: AncillaryKind,
854 current_package: &str,
855 proto_fqn: &str,
856 from_nesting: usize,
857) -> proc_macro2::TokenStream {
858 use crate::idents::make_field_ident;
859 use quote::quote;
860
861 debug_assert!(
862 !proto_fqn.starts_with('.'),
863 "ancillary_prefix expects dotless FQN, got {proto_fqn:?}"
864 );
865
866 let mut supers_tokens = proc_macro2::TokenStream::new();
867 for _ in 0..from_nesting {
868 supers_tokens.extend(quote! { super:: });
869 }
870
871 let sentinel = make_field_ident(SENTINEL_MOD);
872 let kind_segs: Vec<_> = kind
873 .path_segments()
874 .iter()
875 .map(|s| make_field_ident(s))
876 .collect();
877
878 let within_pkg = if current_package.is_empty() {
880 proto_fqn
881 } else {
882 proto_fqn
883 .strip_prefix(current_package)
884 .and_then(|s| s.strip_prefix('.'))
885 .unwrap_or(proto_fqn)
886 };
887 let msg_segs: Vec<_> = within_pkg
888 .split('.')
889 .filter(|s| !s.is_empty())
890 .map(|name| make_field_ident(&to_snake_case(name)))
891 .collect();
892
893 quote! { #supers_tokens #sentinel :: #(#kind_segs ::)* #(#msg_segs ::)* }
894}
895
896pub(crate) fn matches_proto_prefix(prefix: &str, fqn_dotted: &str) -> bool {
901 prefix == "."
902 || prefix == fqn_dotted
903 || (fqn_dotted.starts_with(prefix)
904 && fqn_dotted.as_bytes().get(prefix.len()) == Some(&b'.'))
905}
906
907fn resolve_file_extern<'p>(
916 file_name: &str,
917 file_extern_paths: &'p [(String, String)],
918) -> Option<&'p str> {
919 file_extern_paths
920 .iter()
921 .find(|(name, _)| name == file_name)
922 .map(|(_, rust)| rust.as_str())
923}
924
925pub(crate) fn resolve_extern_prefix(
936 package: &str,
937 extern_paths: &[(String, String)],
938) -> Option<String> {
939 let dotted = format!(".{}", package);
940
941 let mut best: Option<(&str, &str, usize)> = None;
944
945 for (proto_prefix, rust_prefix) in extern_paths {
946 if dotted == *proto_prefix {
947 return Some(rust_prefix.clone());
949 }
950 if let Some(rest) = dotted.strip_prefix(proto_prefix.as_str()) {
951 if proto_prefix == "." || rest.starts_with('.') {
953 let prefix_len = proto_prefix.len();
954 if best.is_none_or(|(_, _, best_len)| prefix_len > best_len) {
955 best = Some((proto_prefix, rust_prefix, prefix_len));
956 }
957 }
958 }
959 }
960
961 let (proto_prefix, rust_prefix, _) = best?;
962 let rest = dotted.strip_prefix(proto_prefix)?;
963 let rest = rest.strip_prefix('.').unwrap_or(rest);
964 let suffix = rest
965 .split('.')
966 .map(to_snake_case)
967 .collect::<Vec<_>>()
968 .join("::");
969 Some(format!("{}::{}", rust_prefix, suffix))
970}
971
972fn resolve_extern_type(fqn: &str, extern_paths: &[(String, String)]) -> Option<String> {
991 if let Some((_, rust)) = extern_paths.iter().find(|(proto, _)| proto == fqn) {
993 return Some(rust.clone());
994 }
995
996 let mut best: Option<(&str, &str, usize)> = None;
998 for (proto_prefix, rust_prefix) in extern_paths {
999 let matches = proto_prefix == "."
1000 || fqn
1001 .strip_prefix(proto_prefix.as_str())
1002 .is_some_and(|rest| rest.starts_with('.'));
1003 if matches && best.is_none_or(|(_, _, best_len)| proto_prefix.len() > best_len) {
1004 best = Some((proto_prefix, rust_prefix, proto_prefix.len()));
1005 }
1006 }
1007
1008 let (proto_prefix, rust_prefix, _) = best?;
1009 let rest = if proto_prefix == "." {
1012 fqn.strip_prefix('.').unwrap_or(fqn)
1013 } else {
1014 fqn.strip_prefix(proto_prefix)
1015 .and_then(|r| r.strip_prefix('.'))
1016 .unwrap_or("")
1017 };
1018 let mut segments = rest.split('.').collect::<Vec<_>>();
1019 let type_name = segments.pop()?;
1021 let mut path = rust_prefix.to_string();
1022 for module in segments {
1023 path.push_str("::");
1024 path.push_str(&to_snake_case(module));
1025 }
1026 path.push_str("::");
1027 path.push_str(type_name);
1028 Some(path)
1029}
1030
1031fn join_mod(module: &str, name: &str) -> String {
1034 if module.is_empty() {
1035 name.to_string()
1036 } else {
1037 format!("{module}::{name}")
1038 }
1039}
1040
1041fn resolve_type_path(
1054 fqn: &str,
1055 name: &str,
1056 file_root: Option<&str>,
1057 local_module: &str,
1058 extern_paths: &[(String, String)],
1059) -> (String, bool) {
1060 if let Some((_, rust)) = extern_paths.iter().find(|(proto, _)| proto == fqn) {
1065 (rust.clone(), true)
1066 } else if let Some(root) = file_root {
1067 (join_mod(root, name), true)
1068 } else if let Some(path) = resolve_extern_type(fqn, extern_paths) {
1069 (path, true)
1070 } else {
1071 (join_mod(local_module, name), false)
1072 }
1073}
1074
1075fn register_nested_types(
1085 type_map: &mut HashMap<String, String>,
1086 package_of: &mut HashMap<String, String>,
1087 package: &str,
1088 parent_fqn: &str,
1089 parent_mod: &str,
1090 msg: &crate::generated::descriptor::DescriptorProto,
1091 extern_paths: &[(String, String)],
1092) {
1093 for nested in &msg.nested_type {
1094 if let Some(name) = &nested.name {
1095 let fqn = format!("{}.{}", parent_fqn, name);
1096 let (rust_path, child_mod) = match extern_paths.iter().find(|(proto, _)| proto == &fqn)
1100 {
1101 Some((_, rust)) => {
1102 let child = match rust.rsplit_once("::") {
1103 Some((parent, _)) => format!("{parent}::{}", to_snake_case(name)),
1104 None => to_snake_case(name),
1105 };
1106 (rust.clone(), child)
1107 }
1108 None => (
1109 format!("{parent_mod}::{name}"),
1110 format!("{parent_mod}::{}", to_snake_case(name)),
1111 ),
1112 };
1113 type_map.insert(fqn.clone(), rust_path);
1114 package_of.insert(fqn.clone(), package.to_string());
1115
1116 register_nested_types(
1118 type_map,
1119 package_of,
1120 package,
1121 &fqn,
1122 &child_mod,
1123 nested,
1124 extern_paths,
1125 );
1126 }
1127 }
1128
1129 for enum_type in &msg.enum_type {
1130 if let Some(name) = &enum_type.name {
1131 let fqn = format!("{}.{}", parent_fqn, name);
1132 let rust_path = extern_paths
1133 .iter()
1134 .find(|(proto, _)| proto == &fqn)
1135 .map(|(_, rust)| rust.clone())
1136 .unwrap_or_else(|| format!("{parent_mod}::{name}"));
1137 type_map.insert(fqn.clone(), rust_path);
1138 package_of.insert(fqn, package.to_string());
1139 }
1140 }
1141}
1142
1143fn register_enum_closedness(
1145 map: &mut HashMap<String, bool>,
1146 fqn: &str,
1147 parent_features: &ResolvedFeatures,
1148 enum_desc: &EnumDescriptorProto,
1149) {
1150 let resolved = features::resolve_child(parent_features, features::enum_features(enum_desc));
1151 let closed = resolved.enum_type == features::EnumType::Closed;
1152 map.insert(fqn.to_string(), closed);
1153}
1154
1155fn register_nested_enum_closedness(
1158 map: &mut HashMap<String, bool>,
1159 parent_fqn: &str,
1160 parent_features: &ResolvedFeatures,
1161 msg: &DescriptorProto,
1162) {
1163 let msg_features = features::resolve_child(parent_features, features::message_features(msg));
1164 for enum_type in &msg.enum_type {
1165 if let Some(name) = &enum_type.name {
1166 let fqn = format!("{}.{}", parent_fqn, name);
1167 register_enum_closedness(map, &fqn, &msg_features, enum_type);
1168 }
1169 }
1170 for nested in &msg.nested_type {
1171 if let Some(name) = &nested.name {
1172 let fqn = format!("{}.{}", parent_fqn, name);
1173 register_nested_enum_closedness(map, &fqn, &msg_features, nested);
1174 }
1175 }
1176}
1177
1178#[cfg(test)]
1179mod tests {
1180 use super::*;
1181 use crate::generated::descriptor::{DescriptorProto, EnumDescriptorProto, FileDescriptorProto};
1182
1183 fn children(segs: &[&str]) -> HashSet<String> {
1184 segs.iter().map(|s| s.to_string()).collect()
1185 }
1186
1187 fn names(ns: &[&str]) -> Vec<String> {
1188 ns.iter().map(|s| s.to_string()).collect()
1189 }
1190
1191 #[test]
1192 fn deconflict_no_collision_keeps_base() {
1193 let out = deconflict_package_modules(&names(&["Oof", "Bar"]), &children(&["other"]));
1195 assert_eq!(out, vec!["oof".to_string(), "bar".to_string()]);
1196 }
1197
1198 #[test]
1199 fn deconflict_single_collision_appends_underscore() {
1200 let out = deconflict_package_modules(&names(&["Oof"]), &children(&["oof"]));
1201 assert_eq!(out, vec!["oof_".to_string()]);
1202 }
1203
1204 #[test]
1205 fn deconflict_repeated_append_when_underscore_slot_also_taken() {
1206 let out = deconflict_package_modules(&names(&["Oof"]), &children(&["oof", "oof_"]));
1208 assert_eq!(out, vec!["oof__".to_string()]);
1209 }
1210
1211 #[test]
1212 fn deconflict_two_messages_racing_to_same_slot_stay_distinct() {
1213 let out = deconflict_package_modules(&names(&["Oof", "Oof_"]), &children(&["oof", "oof_"]));
1216 assert_eq!(out, vec!["oof__".to_string(), "oof___".to_string()]);
1217 let set: HashSet<&String> = out.iter().collect();
1219 assert_eq!(set.len(), out.len());
1220 assert!(!out.contains(&"oof".to_string()) && !out.contains(&"oof_".to_string()));
1221 }
1222
1223 #[test]
1224 fn deconflict_is_independent_of_declaration_order() {
1225 let ch = children(&["oof", "oof_"]);
1227 let fwd = deconflict_package_modules(&names(&["Oof", "Oof_"]), &ch);
1228 let rev = deconflict_package_modules(&names(&["Oof_", "Oof"]), &ch);
1229 assert_eq!(fwd, vec!["oof__".to_string(), "oof___".to_string()]);
1231 assert_eq!(rev, vec!["oof___".to_string(), "oof__".to_string()]);
1232 }
1233
1234 #[test]
1235 fn deconflict_avoids_other_messages_raw_base() {
1236 let out = deconflict_package_modules(&names(&["Oof", "Oof_"]), &children(&["oof"]));
1239 assert_eq!(out, vec!["oof__".to_string(), "oof_".to_string()]);
1241 }
1242
1243 #[test]
1244 fn deconflict_never_yields_the_sentinel() {
1245 let out = deconflict_package_modules(&names(&["Buffa"]), &children(&["__buffa", "buffa"]));
1246 assert_eq!(out, vec!["buffa_".to_string()]);
1248 assert_ne!(out[0], SENTINEL_MOD);
1249 }
1250
1251 #[test]
1252 fn child_package_segments_extracts_immediate_segment() {
1253 let pkgs = children(&["foo", "foo.oof", "foo.bar.baz", "foobar"]);
1254 let mut got: Vec<String> = child_package_segments("foo", &pkgs).into_iter().collect();
1255 got.sort();
1256 assert_eq!(got, vec!["bar".to_string(), "oof".to_string()]);
1258 }
1259
1260 fn make_file(
1261 name: &str,
1262 package: &str,
1263 messages: Vec<DescriptorProto>,
1264 enums: Vec<EnumDescriptorProto>,
1265 ) -> FileDescriptorProto {
1266 FileDescriptorProto {
1267 name: Some(name.to_string()),
1268 package: if package.is_empty() {
1269 None
1270 } else {
1271 Some(package.to_string())
1272 },
1273 message_type: messages,
1274 enum_type: enums,
1275 ..Default::default()
1276 }
1277 }
1278
1279 fn msg(name: &str) -> DescriptorProto {
1280 DescriptorProto {
1281 name: Some(name.to_string()),
1282 ..Default::default()
1283 }
1284 }
1285
1286 fn msg_with_nested(name: &str, nested: Vec<DescriptorProto>) -> DescriptorProto {
1287 DescriptorProto {
1288 name: Some(name.to_string()),
1289 nested_type: nested,
1290 ..Default::default()
1291 }
1292 }
1293
1294 fn msg_with_nested_and_enums(
1295 name: &str,
1296 nested: Vec<DescriptorProto>,
1297 enums: Vec<EnumDescriptorProto>,
1298 ) -> DescriptorProto {
1299 DescriptorProto {
1300 name: Some(name.to_string()),
1301 nested_type: nested,
1302 enum_type: enums,
1303 ..Default::default()
1304 }
1305 }
1306
1307 fn enum_desc(name: &str) -> EnumDescriptorProto {
1308 EnumDescriptorProto {
1309 name: Some(name.to_string()),
1310 ..Default::default()
1311 }
1312 }
1313
1314 fn enum_with_closed_feature(name: &str) -> EnumDescriptorProto {
1315 use crate::generated::descriptor::{feature_set, EnumOptions, FeatureSet};
1316 EnumDescriptorProto {
1317 name: Some(name.to_string()),
1318 options: buffa::MessageField::some(EnumOptions {
1319 features: buffa::MessageField::some(FeatureSet {
1320 enum_type: Some(feature_set::EnumType::CLOSED),
1321 ..Default::default()
1322 }),
1323 ..Default::default()
1324 }),
1325 ..Default::default()
1326 }
1327 }
1328
1329 fn editions_file(
1330 name: &str,
1331 package: &str,
1332 messages: Vec<DescriptorProto>,
1333 enums: Vec<EnumDescriptorProto>,
1334 ) -> FileDescriptorProto {
1335 use crate::generated::descriptor::Edition;
1336 FileDescriptorProto {
1337 name: Some(name.to_string()),
1338 package: Some(package.to_string()),
1339 syntax: Some("editions".to_string()),
1340 edition: Some(Edition::EDITION_2023),
1341 message_type: messages,
1342 enum_type: enums,
1343 ..Default::default()
1344 }
1345 }
1346
1347 #[test]
1350 fn test_message_with_package() {
1351 let files = [make_file(
1352 "test.proto",
1353 "my.package",
1354 vec![msg("Foo")],
1355 vec![],
1356 )];
1357 let config = CodeGenConfig::default();
1358 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1359 assert_eq!(ctx.rust_type(".my.package.Foo"), Some("my::package::Foo"));
1360 }
1361
1362 #[test]
1363 fn test_message_no_package() {
1364 let files = [make_file("test.proto", "", vec![msg("Bar")], vec![])];
1365 let config = CodeGenConfig::default();
1366 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1367 assert_eq!(ctx.rust_type(".Bar"), Some("Bar"));
1368 }
1369
1370 #[test]
1371 fn test_nested_message_uses_module_path() {
1372 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
1373 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
1374 let config = CodeGenConfig::default();
1375 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1376 assert_eq!(ctx.rust_type(".pkg.Outer"), Some("pkg::Outer"));
1377 assert_eq!(ctx.rust_type(".pkg.Outer.Inner"), Some("pkg::outer::Inner"));
1379 }
1380
1381 #[test]
1382 fn test_nested_message_no_package() {
1383 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
1384 let files = [make_file("test.proto", "", vec![outer], vec![])];
1385 let config = CodeGenConfig::default();
1386 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1387 assert_eq!(ctx.rust_type(".Outer"), Some("Outer"));
1388 assert_eq!(ctx.rust_type(".Outer.Inner"), Some("outer::Inner"));
1389 }
1390
1391 #[test]
1392 fn test_deeply_nested_message() {
1393 let deep = msg_with_nested("A", vec![msg_with_nested("B", vec![msg("C")])]);
1394 let files = [make_file("test.proto", "pkg", vec![deep], vec![])];
1395 let config = CodeGenConfig::default();
1396 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1397 assert_eq!(ctx.rust_type(".pkg.A"), Some("pkg::A"));
1398 assert_eq!(ctx.rust_type(".pkg.A.B"), Some("pkg::a::B"));
1399 assert_eq!(ctx.rust_type(".pkg.A.B.C"), Some("pkg::a::b::C"));
1400 }
1401
1402 #[test]
1403 fn test_nested_enum_uses_module_path() {
1404 let outer = msg_with_nested_and_enums("Outer", vec![], vec![enum_desc("Status")]);
1405 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
1406 let config = CodeGenConfig::default();
1407 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1408 assert_eq!(
1409 ctx.rust_type(".pkg.Outer.Status"),
1410 Some("pkg::outer::Status")
1411 );
1412 }
1413
1414 #[test]
1415 fn test_top_level_enum() {
1416 let files = [make_file(
1417 "test.proto",
1418 "pkg",
1419 vec![],
1420 vec![enum_desc("Status")],
1421 )];
1422 let config = CodeGenConfig::default();
1423 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1424 assert_eq!(ctx.rust_type(".pkg.Status"), Some("pkg::Status"));
1425 }
1426
1427 #[test]
1428 fn test_same_named_nested_types_in_different_parents_are_distinct() {
1429 let outer1 = msg_with_nested("Outer1", vec![msg("Inner")]);
1430 let outer2 = msg_with_nested("Outer2", vec![msg("Inner")]);
1431 let files = [make_file("a.proto", "pkg", vec![outer1, outer2], vec![])];
1432 let config = CodeGenConfig::default();
1433 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1434 assert_eq!(
1436 ctx.rust_type(".pkg.Outer1.Inner"),
1437 Some("pkg::outer1::Inner")
1438 );
1439 assert_eq!(
1440 ctx.rust_type(".pkg.Outer2.Inner"),
1441 Some("pkg::outer2::Inner")
1442 );
1443 assert_ne!(
1444 ctx.rust_type(".pkg.Outer1.Inner"),
1445 ctx.rust_type(".pkg.Outer2.Inner")
1446 );
1447 }
1448
1449 #[test]
1450 fn test_multiple_files() {
1451 let files = [
1452 make_file("a.proto", "ns.a", vec![msg("MsgA")], vec![]),
1453 make_file("b.proto", "ns.b", vec![msg("MsgB")], vec![]),
1454 ];
1455 let config = CodeGenConfig::default();
1456 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1457 assert_eq!(ctx.rust_type(".ns.a.MsgA"), Some("ns::a::MsgA"));
1458 assert_eq!(ctx.rust_type(".ns.b.MsgB"), Some("ns::b::MsgB"));
1459 }
1460
1461 #[test]
1471 fn test_extern_path_exact_per_type_match() {
1472 let files = [make_file(
1473 "test.proto",
1474 "test.pkg",
1475 vec![msg("Msg")],
1476 vec![],
1477 )];
1478 let config = CodeGenConfig {
1479 extern_paths: vec![(".test.pkg.Msg".into(), "::ext_crate::Msg".into())],
1480 ..Default::default()
1481 };
1482 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1483 assert_eq!(ctx.rust_type(".test.pkg.Msg"), Some("::ext_crate::Msg"));
1485 }
1486
1487 #[test]
1488 fn test_extern_path_per_type_overrides_package_prefix() {
1489 let files = [make_file(
1493 "test.proto",
1494 "test.pkg",
1495 vec![msg("Msg"), msg("Other")],
1496 vec![],
1497 )];
1498 let config = CodeGenConfig {
1499 extern_paths: vec![
1500 (".test.pkg".into(), "::pkg_crate".into()),
1501 (".test.pkg.Msg".into(), "::ext_crate::Msg".into()),
1502 ],
1503 ..Default::default()
1504 };
1505 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1506 assert_eq!(ctx.rust_type(".test.pkg.Msg"), Some("::ext_crate::Msg"));
1507 assert_eq!(ctx.rust_type(".test.pkg.Other"), Some("::pkg_crate::Other"));
1508 }
1509
1510 #[test]
1511 fn test_extern_path_nested_type_inherits_per_type_override() {
1512 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
1515 let files = [make_file("test.proto", "test.pkg", vec![outer], vec![])];
1516 let config = CodeGenConfig {
1517 extern_paths: vec![(".test.pkg.Outer".into(), "::ext::Outer".into())],
1518 ..Default::default()
1519 };
1520 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1521 assert_eq!(ctx.rust_type(".test.pkg.Outer"), Some("::ext::Outer"));
1522 assert_eq!(
1523 ctx.rust_type(".test.pkg.Outer.Inner"),
1524 Some("::ext::outer::Inner")
1525 );
1526 }
1527
1528 #[test]
1529 fn test_extern_path_exact_per_type_enum() {
1530 let files = [make_file(
1531 "test.proto",
1532 "test.pkg",
1533 vec![],
1534 vec![enum_desc("Status")],
1535 )];
1536 let config = CodeGenConfig {
1537 extern_paths: vec![(".test.pkg.Status".into(), "::ext_crate::Status".into())],
1538 ..Default::default()
1539 };
1540 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1541 assert_eq!(
1542 ctx.rust_type(".test.pkg.Status"),
1543 Some("::ext_crate::Status")
1544 );
1545 }
1546
1547 #[test]
1548 fn test_extern_path_package_prefix_still_resolves() {
1549 let files = [make_file(
1552 "test.proto",
1553 "test.pkg",
1554 vec![msg("Msg")],
1555 vec![],
1556 )];
1557 let config = CodeGenConfig {
1558 extern_paths: vec![(".test.pkg".into(), "::pkg_crate".into())],
1559 ..Default::default()
1560 };
1561 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1562 assert_eq!(ctx.rust_type(".test.pkg.Msg"), Some("::pkg_crate::Msg"));
1563 }
1564
1565 #[test]
1566 fn test_extern_path_per_type_does_not_affect_unmapped_type() {
1567 let files = [make_file(
1569 "test.proto",
1570 "test.pkg",
1571 vec![msg("Msg"), msg("Other")],
1572 vec![],
1573 )];
1574 let config = CodeGenConfig {
1575 extern_paths: vec![(".test.pkg.Msg".into(), "::ext_crate::Msg".into())],
1576 ..Default::default()
1577 };
1578 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1579 assert_eq!(ctx.rust_type(".test.pkg.Msg"), Some("::ext_crate::Msg"));
1580 assert_eq!(ctx.rust_type(".test.pkg.Other"), Some("test::pkg::Other"));
1582 }
1583
1584 #[test]
1585 fn test_keyword_package_segment_in_type_map() {
1586 let files = [make_file(
1589 "latlng.proto",
1590 "google.type",
1591 vec![msg("LatLng")],
1592 vec![],
1593 )];
1594 let config = CodeGenConfig::default();
1595 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1596 assert_eq!(
1597 ctx.rust_type(".google.type.LatLng"),
1598 Some("google::type::LatLng")
1599 );
1600 }
1601
1602 #[test]
1603 fn test_keyword_package_relative_same_package() {
1604 let files = [make_file(
1605 "latlng.proto",
1606 "google.type",
1607 vec![msg("LatLng"), msg("Expr")],
1608 vec![],
1609 )];
1610 let config = CodeGenConfig::default();
1611 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1612 assert_eq!(
1614 ctx.rust_type_relative(".google.type.LatLng", "google.type", 0),
1615 Some("LatLng".into())
1616 );
1617 }
1618
1619 #[test]
1620 fn test_keyword_package_cross_package() {
1621 let files = [
1622 make_file("latlng.proto", "google.type", vec![msg("LatLng")], vec![]),
1623 make_file("svc.proto", "google.cloud", vec![msg("Service")], vec![]),
1624 ];
1625 let config = CodeGenConfig::default();
1626 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1627 assert_eq!(
1630 ctx.rust_type_relative(".google.type.LatLng", "google.cloud", 0),
1631 Some("super::type::LatLng".into())
1632 );
1633 }
1634
1635 #[test]
1636 fn test_keyword_nested_message_module() {
1637 let outer = msg_with_nested("Type", vec![msg("Inner")]);
1639 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
1640 let config = CodeGenConfig::default();
1641 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1642 assert_eq!(ctx.rust_type(".pkg.Type"), Some("pkg::Type"));
1643 assert_eq!(ctx.rust_type(".pkg.Type.Inner"), Some("pkg::type::Inner"));
1644 }
1645
1646 #[test]
1647 fn test_unknown_type_returns_none() {
1648 let files = [make_file("test.proto", "pkg", vec![msg("Foo")], vec![])];
1649 let config = CodeGenConfig::default();
1650 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1651 assert_eq!(ctx.rust_type(".pkg.Unknown"), None);
1652 }
1653
1654 #[test]
1657 fn test_relative_same_package_top_level() {
1658 let files = [make_file("a.proto", "pkg", vec![msg("Foo")], vec![])];
1659 let config = CodeGenConfig::default();
1660 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1661 assert_eq!(
1663 ctx.rust_type_relative(".pkg.Foo", "pkg", 0),
1664 Some("Foo".into())
1665 );
1666 }
1667
1668 #[test]
1669 fn test_relative_cross_package() {
1670 let files = [
1671 make_file("a.proto", "pkg_a", vec![msg("Foo")], vec![]),
1672 make_file("b.proto", "pkg_b", vec![msg("Bar")], vec![]),
1673 ];
1674 let config = CodeGenConfig::default();
1675 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1676 assert_eq!(
1678 ctx.rust_type_relative(".pkg_a.Foo", "pkg_b", 0),
1679 Some("super::pkg_a::Foo".into())
1680 );
1681 }
1682
1683 #[test]
1684 fn test_relative_no_package() {
1685 let files = [make_file("a.proto", "", vec![msg("Foo")], vec![])];
1686 let config = CodeGenConfig::default();
1687 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1688 assert_eq!(ctx.rust_type_relative(".Foo", "", 0), Some("Foo".into()));
1689 }
1690
1691 #[test]
1692 fn test_relative_unknown_returns_none() {
1693 let files = [make_file("a.proto", "pkg", vec![msg("Foo")], vec![])];
1694 let config = CodeGenConfig::default();
1695 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1696 assert_eq!(ctx.rust_type_relative(".pkg.Unknown", "pkg", 0), None);
1697 }
1698
1699 #[test]
1700 fn test_relative_dotted_package() {
1701 let files = [make_file("a.proto", "my.pkg", vec![msg("Foo")], vec![])];
1702 let config = CodeGenConfig::default();
1703 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1704 assert_eq!(
1705 ctx.rust_type_relative(".my.pkg.Foo", "my.pkg", 0),
1706 Some("Foo".into())
1707 );
1708 }
1709
1710 #[test]
1711 fn test_relative_cross_dotted_packages() {
1712 let files = [
1713 make_file(
1714 "timestamp.proto",
1715 "google.protobuf",
1716 vec![msg("Timestamp")],
1717 vec![],
1718 ),
1719 make_file(
1720 "test.proto",
1721 "protobuf_test_messages.proto3",
1722 vec![msg("TestAllTypesProto3")],
1723 vec![],
1724 ),
1725 ];
1726 let config = CodeGenConfig::default();
1727 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1728
1729 assert_eq!(
1731 ctx.rust_type_relative(
1732 ".google.protobuf.Timestamp",
1733 "protobuf_test_messages.proto3",
1734 0,
1735 ),
1736 Some("super::super::google::protobuf::Timestamp".into())
1737 );
1738 }
1739
1740 #[test]
1741 fn test_relative_nested_type_from_same_package() {
1742 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
1744 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
1745 let config = CodeGenConfig::default();
1746 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1747
1748 assert_eq!(
1750 ctx.rust_type_relative(".pkg.Outer.Inner", "pkg", 0),
1751 Some("outer::Inner".into())
1752 );
1753 }
1754
1755 #[test]
1756 fn test_relative_shared_prefix_not_confused() {
1757 let files = [
1758 make_file("ab.proto", "a.b", vec![msg("Msg1")], vec![]),
1759 make_file("abc.proto", "a.bc", vec![msg("Msg2")], vec![]),
1760 ];
1761 let config = CodeGenConfig::default();
1762 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1763
1764 assert_eq!(
1766 ctx.rust_type_relative(".a.b.Msg1", "a.bc", 0),
1767 Some("super::b::Msg1".into())
1768 );
1769 assert_eq!(
1771 ctx.rust_type_relative(".a.bc.Msg2", "a.b", 0),
1772 Some("super::bc::Msg2".into())
1773 );
1774 }
1775
1776 #[test]
1779 fn test_relative_cross_package_nesting_1() {
1780 let outer = msg_with_nested_and_enums("Business", vec![], vec![enum_desc("Status")]);
1784 let files = [
1785 make_file("admin.proto", "a.b.admin.v1", vec![msg("Svc")], vec![]),
1786 make_file("biz.proto", "a.b.v1", vec![outer], vec![]),
1787 ];
1788 let config = CodeGenConfig::default();
1789 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1790
1791 assert_eq!(
1793 ctx.rust_type_relative(".a.b.v1.Business.Status", "a.b.admin.v1", 0),
1794 Some("super::super::v1::business::Status".into())
1795 );
1796 assert_eq!(
1798 ctx.rust_type_relative(".a.b.v1.Business.Status", "a.b.admin.v1", 1),
1799 Some("super::super::super::v1::business::Status".into())
1800 );
1801 }
1802
1803 #[test]
1804 fn test_relative_same_package_nesting_1() {
1805 let files = [make_file(
1807 "test.proto",
1808 "pkg",
1809 vec![msg("Foo"), msg("Bar")],
1810 vec![],
1811 )];
1812 let config = CodeGenConfig::default();
1813 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1814
1815 assert_eq!(
1817 ctx.rust_type_relative(".pkg.Foo", "pkg", 0),
1818 Some("Foo".into())
1819 );
1820 assert_eq!(
1822 ctx.rust_type_relative(".pkg.Foo", "pkg", 1),
1823 Some("super::Foo".into())
1824 );
1825 assert_eq!(
1827 ctx.rust_type_relative(".pkg.Foo", "pkg", 2),
1828 Some("super::super::Foo".into())
1829 );
1830 }
1831
1832 #[test]
1835 fn test_resolve_file_extern_exact_match_only() {
1836 let mappings = [(
1837 "google/protobuf/descriptor.proto".to_string(),
1838 "::buffa_descriptor::generated::descriptor".to_string(),
1839 )];
1840 assert_eq!(
1842 resolve_file_extern("google/protobuf/descriptor.proto", &mappings),
1843 Some("::buffa_descriptor::generated::descriptor"),
1844 );
1845 assert_eq!(
1848 resolve_file_extern("google/protobuf/timestamp.proto", &mappings),
1849 None,
1850 );
1851 assert_eq!(
1853 resolve_file_extern("vendor/google/protobuf/descriptor.proto", &mappings),
1854 None,
1855 );
1856 }
1857
1858 #[test]
1859 fn test_resolve_extern_prefix_exact_match() {
1860 let result = resolve_extern_prefix(
1861 "my.common",
1862 &[(".my.common".into(), "::common_protos".into())],
1863 );
1864 assert_eq!(result, Some("::common_protos".into()));
1865 }
1866
1867 #[test]
1868 fn test_resolve_extern_prefix_sub_package() {
1869 let result = resolve_extern_prefix(
1870 "my.common.sub",
1871 &[(".my.common".into(), "::common_protos".into())],
1872 );
1873 assert_eq!(result, Some("::common_protos::sub".into()));
1874 }
1875
1876 #[test]
1877 fn test_resolve_extern_prefix_no_match() {
1878 let result = resolve_extern_prefix(
1879 "other.pkg",
1880 &[(".my.common".into(), "::common_protos".into())],
1881 );
1882 assert_eq!(result, None);
1883 }
1884
1885 #[test]
1886 fn test_resolve_extern_prefix_partial_name_no_match() {
1887 let result = resolve_extern_prefix(
1889 "my.commonext",
1890 &[(".my.common".into(), "::common_protos".into())],
1891 );
1892 assert_eq!(result, None);
1893 }
1894
1895 #[test]
1896 fn test_resolve_extern_prefix_longest_match_wins() {
1897 let result = resolve_extern_prefix(
1899 "my.common.sub",
1900 &[
1901 (".my".into(), "::crate_a".into()),
1902 (".my.common".into(), "::crate_b".into()),
1903 ],
1904 );
1905 assert_eq!(result, Some("::crate_b::sub".into()));
1906 }
1907
1908 #[test]
1909 fn test_resolve_extern_prefix_catchall() {
1910 let result = resolve_extern_prefix("greet.v1", &[(".".into(), "crate::proto".into())]);
1911 assert_eq!(result, Some("crate::proto::greet::v1".into()));
1912 }
1913
1914 #[test]
1915 fn test_resolve_extern_prefix_catchall_empty_pkg() {
1916 let result = resolve_extern_prefix("", &[(".".into(), "crate::proto".into())]);
1919 assert_eq!(result, Some("crate::proto".into()));
1920 }
1921
1922 #[test]
1923 fn test_resolve_extern_prefix_catchall_longest_wins() {
1924 let result = resolve_extern_prefix(
1927 "google.protobuf",
1928 &[
1929 (".".into(), "crate::proto".into()),
1930 (
1931 ".google.protobuf".into(),
1932 "::buffa_types::google::protobuf".into(),
1933 ),
1934 ],
1935 );
1936 assert_eq!(result, Some("::buffa_types::google::protobuf".into()));
1937 }
1938
1939 #[test]
1940 fn test_resolve_extern_prefix_catchall_keyword_package() {
1941 let result = resolve_extern_prefix("google.type", &[(".".into(), "crate::proto".into())]);
1944 assert_eq!(result, Some("crate::proto::google::type".into()));
1945 }
1946
1947 #[test]
1950 fn test_resolve_extern_type_exact_match() {
1951 let result = resolve_extern_type(
1953 ".google.protobuf.Timestamp",
1954 &[(
1955 ".google.protobuf.Timestamp".into(),
1956 "::pbjson_types::Timestamp".into(),
1957 )],
1958 );
1959 assert_eq!(result, Some("::pbjson_types::Timestamp".into()));
1960 }
1961
1962 #[test]
1963 fn test_resolve_extern_type_exact_wins_over_prefix() {
1964 let result = resolve_extern_type(
1966 ".my.pkg.Msg",
1967 &[
1968 (".my.pkg".into(), "::pkg_crate".into()),
1969 (".my.pkg.Msg".into(), "::ext_crate::Msg".into()),
1970 ],
1971 );
1972 assert_eq!(result, Some("::ext_crate::Msg".into()));
1973 }
1974
1975 #[test]
1976 fn test_resolve_extern_type_package_prefix_appends_type() {
1977 let result = resolve_extern_type(
1980 ".my.common.sub.Msg",
1981 &[(".my.common".into(), "::common_protos".into())],
1982 );
1983 assert_eq!(result, Some("::common_protos::sub::Msg".into()));
1984 }
1985
1986 #[test]
1987 fn test_resolve_extern_type_catchall() {
1988 let result = resolve_extern_type(".greet.v1.Hello", &[(".".into(), "crate::proto".into())]);
1989 assert_eq!(result, Some("crate::proto::greet::v1::Hello".into()));
1990 }
1991
1992 #[test]
1993 fn test_resolve_extern_type_no_match() {
1994 let result = resolve_extern_type(
1995 ".other.pkg.Msg",
1996 &[(".my.common".into(), "::common_protos".into())],
1997 );
1998 assert_eq!(result, None);
1999 }
2000
2001 #[test]
2002 fn test_resolve_extern_type_partial_name_no_match() {
2003 let result = resolve_extern_type(
2005 ".my.commonext.Msg",
2006 &[(".my.common".into(), "::common_protos".into())],
2007 );
2008 assert_eq!(result, None);
2009 }
2010
2011 #[test]
2014 fn test_split_extern_top_level() {
2015 let outer = msg_with_nested("Value", vec![msg("Inner")]);
2016 let files = [make_file(
2017 "struct.proto",
2018 "google.protobuf",
2019 vec![outer],
2020 vec![],
2021 )];
2022 let config = CodeGenConfig::default();
2023 let extern_paths = vec![(
2024 ".google.protobuf".into(),
2025 "::buffa_types::google::protobuf".into(),
2026 )];
2027 let ctx = CodeGenContext::new(&files, &config, &extern_paths);
2028
2029 let split = ctx
2030 .rust_type_relative_split(".google.protobuf.Value", "my.pkg", 3)
2031 .expect("type resolves");
2032 assert!(split.is_extern);
2033 assert_eq!(split.to_package, "::buffa_types::google::protobuf");
2035 assert_eq!(split.within_package, "Value");
2036 }
2037
2038 #[test]
2039 fn test_split_extern_nested_type() {
2040 let outer = msg_with_nested("Value", vec![msg("Inner")]);
2045 let files = [make_file(
2046 "struct.proto",
2047 "google.protobuf",
2048 vec![outer],
2049 vec![],
2050 )];
2051 let config = CodeGenConfig::default();
2052 let extern_paths = vec![(
2053 ".google.protobuf".into(),
2054 "::buffa_types::google::protobuf".into(),
2055 )];
2056 let ctx = CodeGenContext::new(&files, &config, &extern_paths);
2057
2058 let split = ctx
2059 .rust_type_relative_split(".google.protobuf.Value.Inner", "my.pkg", 0)
2060 .expect("nested type resolves");
2061 assert!(split.is_extern);
2062 assert_eq!(split.to_package, "::buffa_types::google::protobuf");
2063 assert_eq!(split.within_package, "value::Inner");
2064 }
2065
2066 #[test]
2067 fn test_split_per_type_extern_override() {
2068 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
2074 let files = [make_file("custom.proto", "my.pkg", vec![outer], vec![])];
2075 let config = CodeGenConfig {
2076 extern_paths: vec![(".my.pkg.Outer".into(), "::ext::custom::Outer".into())],
2077 ..Default::default()
2078 };
2079 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2080
2081 let split = ctx
2082 .rust_type_relative_split(".my.pkg.Outer", "other.pkg", 2)
2083 .expect("overridden type resolves");
2084 assert!(split.is_extern);
2085 assert_eq!(split.to_package, "::ext::custom");
2086 assert_eq!(split.within_package, "Outer");
2087
2088 let nested = ctx
2091 .rust_type_relative_split(".my.pkg.Outer.Inner", "other.pkg", 0)
2092 .expect("nested type resolves");
2093 assert!(nested.is_extern);
2094 assert_eq!(nested.to_package, "::ext::custom");
2095 assert_eq!(nested.within_package, "outer::Inner");
2096 }
2097
2098 #[test]
2099 fn test_extern_path_top_level_message() {
2100 let files = [make_file(
2101 "common.proto",
2102 "my.common",
2103 vec![msg("SharedMsg")],
2104 vec![],
2105 )];
2106 let config = CodeGenConfig {
2107 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
2108 ..Default::default()
2109 };
2110 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2111 assert_eq!(
2112 ctx.rust_type(".my.common.SharedMsg"),
2113 Some("::common_protos::SharedMsg")
2114 );
2115 }
2116
2117 #[test]
2118 fn test_extern_path_nested_message() {
2119 let files = [make_file(
2120 "common.proto",
2121 "my.common",
2122 vec![msg_with_nested("Outer", vec![msg("Inner")])],
2123 vec![],
2124 )];
2125 let config = CodeGenConfig {
2126 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
2127 ..Default::default()
2128 };
2129 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2130 assert_eq!(
2131 ctx.rust_type(".my.common.Outer"),
2132 Some("::common_protos::Outer")
2133 );
2134 assert_eq!(
2135 ctx.rust_type(".my.common.Outer.Inner"),
2136 Some("::common_protos::outer::Inner")
2137 );
2138 }
2139
2140 #[test]
2141 fn test_extern_path_enum() {
2142 let files = [make_file(
2143 "common.proto",
2144 "my.common",
2145 vec![],
2146 vec![enum_desc("Status")],
2147 )];
2148 let config = CodeGenConfig {
2149 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
2150 ..Default::default()
2151 };
2152 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2153 assert_eq!(
2154 ctx.rust_type(".my.common.Status"),
2155 Some("::common_protos::Status")
2156 );
2157 }
2158
2159 #[test]
2160 fn test_extern_path_does_not_affect_other_packages() {
2161 let files = [
2162 make_file("common.proto", "my.common", vec![msg("SharedMsg")], vec![]),
2163 make_file(
2164 "service.proto",
2165 "my.service",
2166 vec![msg("MyService")],
2167 vec![],
2168 ),
2169 ];
2170 let config = CodeGenConfig {
2171 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
2172 ..Default::default()
2173 };
2174 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2175 assert_eq!(
2177 ctx.rust_type(".my.common.SharedMsg"),
2178 Some("::common_protos::SharedMsg")
2179 );
2180 assert_eq!(
2182 ctx.rust_type(".my.service.MyService"),
2183 Some("my::service::MyService")
2184 );
2185 }
2186
2187 #[test]
2188 fn test_extern_path_relative_returns_absolute() {
2189 let files = [
2192 make_file("common.proto", "my.common", vec![msg("SharedMsg")], vec![]),
2193 make_file(
2194 "service.proto",
2195 "my.service",
2196 vec![msg("MyService")],
2197 vec![],
2198 ),
2199 ];
2200 let config = CodeGenConfig {
2201 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
2202 ..Default::default()
2203 };
2204 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2205 assert_eq!(
2207 ctx.rust_type_relative(".my.common.SharedMsg", "my.service", 0),
2208 Some("::common_protos::SharedMsg".into())
2209 );
2210 }
2211
2212 #[test]
2215 fn test_is_enum_closed_proto3_default_open() {
2216 let files = [make_file("a.proto", "p", vec![], vec![enum_desc("E")])];
2217 let config = CodeGenConfig::default();
2218 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2219 assert_eq!(ctx.is_enum_closed(".p.E"), Some(true));
2223 }
2224
2225 #[test]
2226 fn test_is_enum_closed_editions_default_open() {
2227 let files = [editions_file("a.proto", "p", vec![], vec![enum_desc("E")])];
2228 let config = CodeGenConfig::default();
2229 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2230 assert_eq!(ctx.is_enum_closed(".p.E"), Some(false));
2232 }
2233
2234 #[test]
2235 fn test_is_enum_closed_per_enum_override() {
2236 let files = [editions_file(
2239 "a.proto",
2240 "p",
2241 vec![],
2242 vec![enum_desc("Open"), enum_with_closed_feature("Closed")],
2243 )];
2244 let config = CodeGenConfig::default();
2245 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2246 assert_eq!(ctx.is_enum_closed(".p.Open"), Some(false));
2247 assert_eq!(ctx.is_enum_closed(".p.Closed"), Some(true));
2248 }
2249
2250 #[test]
2251 fn test_is_enum_closed_nested_per_enum_override() {
2252 let files = [editions_file(
2254 "a.proto",
2255 "p",
2256 vec![msg_with_nested_and_enums(
2257 "M",
2258 vec![],
2259 vec![enum_with_closed_feature("Inner")],
2260 )],
2261 vec![],
2262 )];
2263 let config = CodeGenConfig::default();
2264 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2265 assert_eq!(ctx.is_enum_closed(".p.M.Inner"), Some(true));
2266 }
2267
2268 #[test]
2269 fn test_is_enum_closed_unknown_enum_returns_none() {
2270 let files = [editions_file("a.proto", "p", vec![], vec![])];
2271 let config = CodeGenConfig::default();
2272 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2273 assert_eq!(ctx.is_enum_closed(".other.Unknown"), None);
2275 }
2276
2277 #[test]
2278 fn test_for_generate_auto_injects_wkt_mapping() {
2279 let ts_msg = DescriptorProto {
2282 name: Some("Timestamp".into()),
2283 ..Default::default()
2284 };
2285 let files = [FileDescriptorProto {
2286 name: Some("google/protobuf/timestamp.proto".into()),
2287 package: Some("google.protobuf".into()),
2288 syntax: Some("proto3".into()),
2289 message_type: vec![ts_msg],
2290 ..Default::default()
2291 }];
2292 let config = CodeGenConfig::default();
2293 let ctx = CodeGenContext::for_generate(&files, &["other.proto".into()], &config);
2295 assert_eq!(
2296 ctx.rust_type(".google.protobuf.Timestamp"),
2297 Some("::buffa_types::google::protobuf::Timestamp"),
2298 "WKT auto-mapping must be applied via for_generate"
2299 );
2300 }
2301
2302 #[test]
2303 fn test_for_generate_suppresses_wkt_when_generating_wkt() {
2304 let ts_msg = DescriptorProto {
2307 name: Some("Timestamp".into()),
2308 ..Default::default()
2309 };
2310 let files = [FileDescriptorProto {
2311 name: Some("google/protobuf/timestamp.proto".into()),
2312 package: Some("google.protobuf".into()),
2313 syntax: Some("proto3".into()),
2314 message_type: vec![ts_msg],
2315 ..Default::default()
2316 }];
2317 let config = CodeGenConfig::default();
2318 let ctx = CodeGenContext::for_generate(
2319 &files,
2320 &["google/protobuf/timestamp.proto".into()],
2321 &config,
2322 );
2323 assert_eq!(
2325 ctx.rust_type(".google.protobuf.Timestamp"),
2326 Some("google::protobuf::Timestamp")
2327 );
2328 }
2329
2330 #[test]
2333 fn test_matching_attributes_catchall() {
2334 let attrs = vec![(".".into(), "#[derive(Foo)]".into())];
2336 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
2337 assert!(result.to_string().contains("derive"));
2338 }
2339
2340 #[test]
2341 fn test_matching_attributes_exact_match() {
2342 let attrs = vec![(".my.pkg.MyMessage".into(), "#[derive(Bar)]".into())];
2343 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
2344 assert!(result.to_string().contains("derive"));
2345 }
2346
2347 #[test]
2348 fn test_matching_attributes_package_prefix() {
2349 let attrs = vec![(".my.pkg".into(), "#[derive(Baz)]".into())];
2350 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
2351 assert!(result.to_string().contains("derive"));
2352 }
2353
2354 #[test]
2355 fn test_matching_attributes_no_partial_segment_match() {
2356 let attrs = vec![(".my.pk".into(), "#[derive(Bad)]".into())];
2358 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
2359 assert!(result.is_empty());
2360 }
2361
2362 #[test]
2363 fn test_matching_attributes_no_match() {
2364 let attrs = vec![(".other.pkg".into(), "#[derive(Nope)]".into())];
2365 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
2366 assert!(result.is_empty());
2367 }
2368
2369 #[test]
2370 fn test_matching_attributes_multiple_accumulate() {
2371 let attrs = vec![
2373 (".".into(), "#[derive(A)]".into()),
2374 (".my.pkg".into(), "#[derive(B)]".into()),
2375 ];
2376 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
2377 let s = result.to_string();
2378 assert!(s.contains("A") && s.contains("B"));
2379 }
2380
2381 #[test]
2382 fn test_matching_attributes_invalid_attr_errors() {
2383 let attrs = vec![(".".into(), "not valid {{{{".into())];
2386 let err = CodeGenContext::matching_attributes(&attrs, "my.pkg.Msg").unwrap_err();
2387 assert!(matches!(
2388 err,
2389 crate::CodeGenError::InvalidCustomAttribute { .. }
2390 ));
2391 }
2392
2393 #[test]
2394 fn test_matches_proto_prefix_catchall() {
2395 assert!(matches_proto_prefix(".", ".anything.here"));
2396 assert!(matches_proto_prefix(".", "."));
2397 }
2398
2399 #[test]
2400 fn test_matches_proto_prefix_segment_boundary() {
2401 assert!(!matches_proto_prefix(".my.pk", ".my.pkg.Msg"));
2403 assert!(matches_proto_prefix(".my.pkg", ".my.pkg.Msg"));
2405 }
2406}