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 unboxed_oneof_variants: HashSet<String>,
98 warnings: std::cell::RefCell<Vec<crate::CodeGenWarning>>,
110}
111
112fn child_package_segments(package: &str, all_packages: &HashSet<String>) -> HashSet<String> {
119 let prefix = if package.is_empty() {
120 String::new()
121 } else {
122 format!("{package}.")
123 };
124 all_packages
125 .iter()
126 .filter_map(|p| {
127 let rest = if package.is_empty() {
128 Some(p.as_str())
129 } else {
130 p.strip_prefix(&prefix)
131 };
132 rest.filter(|r| !r.is_empty())
133 .map(|r| r.split('.').next().unwrap_or(r).to_string())
134 })
135 .collect()
136}
137
138fn deconflict_package_modules(message_names: &[String], children: &HashSet<String>) -> Vec<String> {
158 let bases: Vec<String> = message_names.iter().map(|n| to_snake_case(n)).collect();
159 let mut taken: HashSet<String> = children.clone();
162 taken.insert(SENTINEL_MOD.to_string());
163 taken.extend(bases.iter().cloned());
164
165 let mut out = bases.clone();
170 let mut order: Vec<usize> = (0..bases.len()).collect();
171 order.sort_by(|&a, &b| bases[a].cmp(&bases[b]));
172 for i in order {
173 if !children.contains(&bases[i]) {
174 continue;
175 }
176 let mut candidate = format!("{}_", bases[i]);
177 while taken.contains(&candidate) {
178 candidate.push('_');
179 }
180 taken.insert(candidate.clone());
181 out[i] = candidate;
182 }
183 out
184}
185
186impl<'a> CodeGenContext<'a> {
187 pub fn new(
199 files: &'a [FileDescriptorProto],
200 config: &'a CodeGenConfig,
201 effective_extern_paths: &[(String, String)],
202 ) -> Self {
203 Self::with_extern_resolution(files, config, effective_extern_paths, &[])
204 }
205
206 pub(crate) fn with_extern_resolution(
229 files: &'a [FileDescriptorProto],
230 config: &'a CodeGenConfig,
231 effective_extern_paths: &[(String, String)],
232 file_extern_paths: &[(String, String)],
233 ) -> Self {
234 let mut type_map = HashMap::new();
235 let mut package_of = HashMap::new();
236 let mut enum_closedness = HashMap::new();
237 let mut comment_map = HashMap::new();
238 let mut nested_module_names = HashMap::new();
239 let unboxed_oneof_variants =
240 crate::oneof::resolve_unboxed_variants(files, &config.unboxed_oneof_fields);
241
242 let mut all_packages: HashSet<String> = HashSet::new();
248 let mut pkg_message_names: HashMap<String, Vec<String>> = HashMap::new();
249 for file in files {
250 let package = file.package.as_deref().unwrap_or("");
251 let is_extern = file
252 .name
253 .as_deref()
254 .and_then(|n| resolve_file_extern(n, file_extern_paths))
255 .is_some()
256 || resolve_extern_prefix(package, effective_extern_paths).is_some();
257 if is_extern {
258 continue;
259 }
260 all_packages.insert(package.to_string());
261 for msg in &file.message_type {
262 if let Some(name) = &msg.name {
263 let fqn = if package.is_empty() {
267 format!(".{name}")
268 } else {
269 format!(".{package}.{name}")
270 };
271 if effective_extern_paths
272 .iter()
273 .any(|(proto, _)| proto == &fqn)
274 {
275 continue;
276 }
277 pkg_message_names
278 .entry(package.to_string())
279 .or_default()
280 .push(name.clone());
281 }
282 }
283 }
284
285 for (package, names) in &pkg_message_names {
290 let children = child_package_segments(package, &all_packages);
291 let modules = deconflict_package_modules(names, &children);
292 for (name, module) in names.iter().zip(modules) {
293 let fqn = if package.is_empty() {
294 format!(".{name}")
295 } else {
296 format!(".{package}.{name}")
297 };
298 nested_module_names.insert(fqn, module);
299 }
300 }
301
302 for file in files {
303 comment_map.extend(crate::comments::fqn_comments(file));
304 let package = file.package.as_deref().unwrap_or("");
305 let file_features = features::for_file(file);
306 let proto_prefix = if package.is_empty() {
307 String::from(".")
308 } else {
309 format!(".{}.", package)
310 };
311
312 let file_root = file
322 .name
323 .as_deref()
324 .and_then(|n| resolve_file_extern(n, file_extern_paths));
325 let local_module = package.replace('.', "::");
326
327 for msg in &file.message_type {
329 if let Some(name) = &msg.name {
330 let fqn = format!("{}{}", proto_prefix, name);
331 let (rust_path, is_extern) = resolve_type_path(
332 &fqn,
333 name,
334 file_root,
335 &local_module,
336 effective_extern_paths,
337 );
338
339 let parent_mod = if is_extern {
347 match rust_path.rsplit_once("::") {
348 Some((parent, _)) => format!("{parent}::{}", to_snake_case(name)),
349 None => to_snake_case(name),
350 }
351 } else {
352 let snake = nested_module_names
353 .get(&fqn)
354 .cloned()
355 .unwrap_or_else(|| to_snake_case(name));
356 join_mod(&local_module, &snake)
357 };
358
359 type_map.insert(fqn.clone(), rust_path);
360 package_of.insert(fqn.clone(), package.to_string());
361 register_nested_types(
362 &mut type_map,
363 &mut package_of,
364 package,
365 &fqn,
366 &parent_mod,
367 msg,
368 effective_extern_paths,
369 );
370 register_nested_enum_closedness(
371 &mut enum_closedness,
372 &fqn,
373 &file_features,
374 msg,
375 );
376 }
377 }
378
379 for enum_type in &file.enum_type {
381 if let Some(name) = &enum_type.name {
382 let fqn = format!("{}{}", proto_prefix, name);
383 let (rust_path, _) = resolve_type_path(
384 &fqn,
385 name,
386 file_root,
387 &local_module,
388 effective_extern_paths,
389 );
390 type_map.insert(fqn.clone(), rust_path);
391 package_of.insert(fqn.clone(), package.to_string());
392 register_enum_closedness(&mut enum_closedness, &fqn, &file_features, enum_type);
393 }
394 }
395 }
396
397 Self {
398 files,
399 config,
400 type_map,
401 package_of,
402 enum_closedness,
403 comment_map,
404 nested_module_names,
405 unboxed_oneof_variants,
406 warnings: std::cell::RefCell::new(Vec::new()),
407 }
408 }
409
410 pub(crate) fn warn(&self, warning: crate::CodeGenWarning) {
412 self.warnings.borrow_mut().push(warning);
413 }
414
415 pub(crate) fn take_warnings(&self) -> Vec<crate::CodeGenWarning> {
421 self.warnings.take()
422 }
423
424 pub fn nested_module_name(&self, package: &str, name: &str) -> String {
433 let fqn = if package.is_empty() {
434 format!(".{name}")
435 } else {
436 format!(".{package}.{name}")
437 };
438 self.nested_module_names
439 .get(&fqn)
440 .cloned()
441 .unwrap_or_else(|| to_snake_case(name))
442 }
443
444 pub fn for_generate(
458 files: &'a [FileDescriptorProto],
459 files_to_generate: &[String],
460 config: &'a CodeGenConfig,
461 ) -> Self {
462 let paths = crate::effective_extern_paths(files, files_to_generate, config);
463 let file_paths = crate::effective_file_extern_paths(files_to_generate, config);
464 Self::with_extern_resolution(files, config, &paths, &file_paths)
465 }
466
467 pub fn rust_type(&self, proto_fqn: &str) -> Option<&str> {
469 self.type_map.get(proto_fqn).map(|s| s.as_str())
470 }
471
472 pub fn comment(&self, fqn: &str) -> Option<&str> {
481 self.comment_map.get(fqn).map(|s| s.as_str())
482 }
483
484 pub fn is_enum_closed(&self, proto_fqn: &str) -> Option<bool> {
491 self.enum_closedness.get(proto_fqn).copied()
492 }
493
494 pub fn rust_type_relative(
509 &self,
510 proto_fqn: &str,
511 current_package: &str,
512 nesting: usize,
513 ) -> Option<String> {
514 let full_path = self.type_map.get(proto_fqn)?;
515
516 if full_path.starts_with("::") || full_path.starts_with("crate::") {
519 return Some(full_path.clone());
520 }
521
522 let target_package = self
523 .package_of
524 .get(proto_fqn)
525 .map(|s| s.as_str())
526 .unwrap_or("");
527
528 let target_rust_module = target_package.replace('.', "::");
531 let type_suffix = if target_rust_module.is_empty() {
532 full_path.as_str()
533 } else {
534 full_path
535 .strip_prefix(&format!("{}::", target_rust_module))
536 .unwrap_or(full_path)
537 };
538
539 if current_package == target_package {
540 if nesting == 0 {
542 return Some(type_suffix.to_string());
543 }
544 let supers = (0..nesting).map(|_| "super").collect::<Vec<_>>().join("::");
545 return Some(format!("{}::{}", supers, type_suffix));
546 }
547
548 let current_parts: Vec<&str> = if current_package.is_empty() {
550 vec![]
551 } else {
552 current_package.split('.').collect()
553 };
554 let target_parts: Vec<&str> = if target_package.is_empty() {
555 vec![]
556 } else {
557 target_package.split('.').collect()
558 };
559
560 let common_len = current_parts
562 .iter()
563 .zip(&target_parts)
564 .take_while(|(a, b)| a == b)
565 .count();
566
567 let up_count = (current_parts.len() - common_len) + nesting;
570
571 let down_parts = &target_parts[common_len..];
573
574 let mut segments: Vec<&str> = vec!["super"; up_count];
575 segments.extend_from_slice(down_parts);
576
577 let mut result = segments.join("::");
579 if !result.is_empty() {
580 result.push_str("::");
581 }
582 result.push_str(type_suffix);
583
584 Some(result)
585 }
586
587 pub fn rust_type_relative_split(
599 &self,
600 proto_fqn: &str,
601 current_package: &str,
602 nesting: usize,
603 ) -> Option<SplitPath> {
604 let full_path = self.type_map.get(proto_fqn)?;
605
606 let target_package = self
607 .package_of
608 .get(proto_fqn)
609 .map(|s| s.as_str())
610 .unwrap_or("");
611
612 let target_rust_module = if full_path.starts_with("::") || full_path.starts_with("crate::")
618 {
619 let fqn_no_dot = proto_fqn.strip_prefix('.').unwrap_or(proto_fqn);
628 let within_proto = if target_package.is_empty() {
629 fqn_no_dot
630 } else {
631 fqn_no_dot
632 .strip_prefix(target_package)
633 .and_then(|s| s.strip_prefix('.'))
634 .unwrap_or(fqn_no_dot)
635 };
636 let within_segs = within_proto.split('.').count();
650 let full_segs: Vec<&str> = full_path.split("::").collect();
651 let cut = full_segs.len().saturating_sub(within_segs);
652 full_segs[..cut].join("::")
653 } else {
654 target_package.replace('.', "::")
655 };
656
657 let type_suffix = if target_rust_module.is_empty() {
658 full_path.as_str()
659 } else {
660 full_path
661 .strip_prefix(&format!("{}::", target_rust_module))
662 .unwrap_or(full_path)
663 };
664
665 if full_path.starts_with("::") || full_path.starts_with("crate::") {
667 return Some(SplitPath {
668 to_package: target_rust_module,
669 within_package: type_suffix.to_string(),
670 is_extern: true,
671 });
672 }
673
674 if current_package == target_package {
675 let to_package = if nesting == 0 {
676 String::new()
677 } else {
678 (0..nesting).map(|_| "super").collect::<Vec<_>>().join("::")
679 };
680 return Some(SplitPath {
681 to_package,
682 within_package: type_suffix.to_string(),
683 is_extern: false,
684 });
685 }
686
687 let current_parts: Vec<&str> = if current_package.is_empty() {
689 vec![]
690 } else {
691 current_package.split('.').collect()
692 };
693 let target_parts: Vec<&str> = if target_package.is_empty() {
694 vec![]
695 } else {
696 target_package.split('.').collect()
697 };
698 let common_len = current_parts
699 .iter()
700 .zip(&target_parts)
701 .take_while(|(a, b)| a == b)
702 .count();
703 let up_count = (current_parts.len() - common_len) + nesting;
704 let down_parts = &target_parts[common_len..];
705
706 let mut segments: Vec<&str> = vec!["super"; up_count];
707 segments.extend_from_slice(down_parts);
708
709 Some(SplitPath {
710 to_package: segments.join("::"),
711 within_package: type_suffix.to_string(),
712 is_extern: false,
713 })
714 }
715
716 pub(crate) fn matching_attributes(
729 attrs: &[(String, String)],
730 fqn: &str,
731 ) -> Result<proc_macro2::TokenStream, crate::CodeGenError> {
732 if attrs.is_empty() {
733 return Ok(proc_macro2::TokenStream::new());
734 }
735 let fqn_dotted = format!(".{fqn}");
736 let mut tokens = proc_macro2::TokenStream::new();
737 for (prefix, attr_str) in attrs {
738 if matches_proto_prefix(prefix, &fqn_dotted) {
739 let parsed =
740 syn::parse_str::<proc_macro2::TokenStream>(attr_str).map_err(|err| {
741 crate::CodeGenError::InvalidCustomAttribute {
742 path: prefix.clone(),
743 attribute: attr_str.clone(),
744 detail: err.to_string(),
745 }
746 })?;
747 tokens.extend(parsed);
748 }
749 }
750 Ok(tokens)
751 }
752
753 pub fn use_bytes_type(&self, field_fqn: &str) -> bool {
761 self.config
762 .bytes_fields
763 .iter()
764 .any(|prefix| matches_proto_prefix(prefix, field_fqn))
765 }
766
767 pub fn oneof_unboxed(&self, variant_fqn: &str) -> bool {
777 self.unboxed_oneof_variants.contains(variant_fqn)
778 }
779
780 pub fn string_repr(&self, field_fqn: &str) -> crate::StringRepr {
790 self.config
791 .string_fields
792 .iter()
793 .rev()
794 .find(|(prefix, _)| matches_proto_prefix(prefix, field_fqn))
795 .map_or(crate::StringRepr::default(), |(_, repr)| *repr)
796 }
797}
798
799#[derive(Clone, Copy)]
806pub(crate) struct MessageScope<'a> {
807 pub ctx: &'a CodeGenContext<'a>,
809 pub current_package: &'a str,
811 pub proto_fqn: &'a str,
814 pub features: &'a ResolvedFeatures,
816 pub nesting: usize,
820}
821
822impl<'a> MessageScope<'a> {
823 pub fn nested(&self, proto_fqn: &'a str, features: &'a ResolvedFeatures) -> MessageScope<'a> {
825 MessageScope {
826 ctx: self.ctx,
827 current_package: self.current_package,
828 proto_fqn,
829 features,
830 nesting: self.nesting + 1,
831 }
832 }
833}
834
835#[derive(Debug, Clone, Copy, PartialEq, Eq)]
840pub(crate) enum AncillaryKind {
841 Oneof,
843 View,
845 ViewOneof,
847}
848
849impl AncillaryKind {
850 fn path_segments(self) -> &'static [&'static str] {
851 match self {
852 Self::Oneof => &["oneof"],
853 Self::View => &["view"],
854 Self::ViewOneof => &["view", "oneof"],
855 }
856 }
857}
858
859pub(crate) fn ancillary_prefix(
874 kind: AncillaryKind,
875 current_package: &str,
876 proto_fqn: &str,
877 from_nesting: usize,
878) -> proc_macro2::TokenStream {
879 use crate::idents::make_field_ident;
880 use quote::quote;
881
882 debug_assert!(
883 !proto_fqn.starts_with('.'),
884 "ancillary_prefix expects dotless FQN, got {proto_fqn:?}"
885 );
886
887 let mut supers_tokens = proc_macro2::TokenStream::new();
888 for _ in 0..from_nesting {
889 supers_tokens.extend(quote! { super:: });
890 }
891
892 let sentinel = make_field_ident(SENTINEL_MOD);
893 let kind_segs: Vec<_> = kind
894 .path_segments()
895 .iter()
896 .map(|s| make_field_ident(s))
897 .collect();
898
899 let within_pkg = if current_package.is_empty() {
901 proto_fqn
902 } else {
903 proto_fqn
904 .strip_prefix(current_package)
905 .and_then(|s| s.strip_prefix('.'))
906 .unwrap_or(proto_fqn)
907 };
908 let msg_segs: Vec<_> = within_pkg
909 .split('.')
910 .filter(|s| !s.is_empty())
911 .map(|name| make_field_ident(&to_snake_case(name)))
912 .collect();
913
914 quote! { #supers_tokens #sentinel :: #(#kind_segs ::)* #(#msg_segs ::)* }
915}
916
917pub(crate) fn matches_proto_prefix(prefix: &str, fqn_dotted: &str) -> bool {
922 prefix == "."
923 || prefix == fqn_dotted
924 || (fqn_dotted.starts_with(prefix)
925 && fqn_dotted.as_bytes().get(prefix.len()) == Some(&b'.'))
926}
927
928fn resolve_file_extern<'p>(
937 file_name: &str,
938 file_extern_paths: &'p [(String, String)],
939) -> Option<&'p str> {
940 file_extern_paths
941 .iter()
942 .find(|(name, _)| name == file_name)
943 .map(|(_, rust)| rust.as_str())
944}
945
946pub(crate) fn resolve_extern_prefix(
957 package: &str,
958 extern_paths: &[(String, String)],
959) -> Option<String> {
960 let dotted = format!(".{}", package);
961
962 let mut best: Option<(&str, &str, usize)> = None;
965
966 for (proto_prefix, rust_prefix) in extern_paths {
967 if dotted == *proto_prefix {
968 return Some(rust_prefix.clone());
970 }
971 if let Some(rest) = dotted.strip_prefix(proto_prefix.as_str()) {
972 if proto_prefix == "." || rest.starts_with('.') {
974 let prefix_len = proto_prefix.len();
975 if best.is_none_or(|(_, _, best_len)| prefix_len > best_len) {
976 best = Some((proto_prefix, rust_prefix, prefix_len));
977 }
978 }
979 }
980 }
981
982 let (proto_prefix, rust_prefix, _) = best?;
983 let rest = dotted.strip_prefix(proto_prefix)?;
984 let rest = rest.strip_prefix('.').unwrap_or(rest);
985 let suffix = rest
986 .split('.')
987 .map(to_snake_case)
988 .collect::<Vec<_>>()
989 .join("::");
990 Some(format!("{}::{}", rust_prefix, suffix))
991}
992
993fn resolve_extern_type(fqn: &str, extern_paths: &[(String, String)]) -> Option<String> {
1012 if let Some((_, rust)) = extern_paths.iter().find(|(proto, _)| proto == fqn) {
1014 return Some(rust.clone());
1015 }
1016
1017 let mut best: Option<(&str, &str, usize)> = None;
1019 for (proto_prefix, rust_prefix) in extern_paths {
1020 let matches = proto_prefix == "."
1021 || fqn
1022 .strip_prefix(proto_prefix.as_str())
1023 .is_some_and(|rest| rest.starts_with('.'));
1024 if matches && best.is_none_or(|(_, _, best_len)| proto_prefix.len() > best_len) {
1025 best = Some((proto_prefix, rust_prefix, proto_prefix.len()));
1026 }
1027 }
1028
1029 let (proto_prefix, rust_prefix, _) = best?;
1030 let rest = if proto_prefix == "." {
1033 fqn.strip_prefix('.').unwrap_or(fqn)
1034 } else {
1035 fqn.strip_prefix(proto_prefix)
1036 .and_then(|r| r.strip_prefix('.'))
1037 .unwrap_or("")
1038 };
1039 let mut segments = rest.split('.').collect::<Vec<_>>();
1040 let type_name = segments.pop()?;
1042 let mut path = rust_prefix.to_string();
1043 for module in segments {
1044 path.push_str("::");
1045 path.push_str(&to_snake_case(module));
1046 }
1047 path.push_str("::");
1048 path.push_str(type_name);
1049 Some(path)
1050}
1051
1052fn join_mod(module: &str, name: &str) -> String {
1055 if module.is_empty() {
1056 name.to_string()
1057 } else {
1058 format!("{module}::{name}")
1059 }
1060}
1061
1062fn resolve_type_path(
1075 fqn: &str,
1076 name: &str,
1077 file_root: Option<&str>,
1078 local_module: &str,
1079 extern_paths: &[(String, String)],
1080) -> (String, bool) {
1081 if let Some((_, rust)) = extern_paths.iter().find(|(proto, _)| proto == fqn) {
1086 (rust.clone(), true)
1087 } else if let Some(root) = file_root {
1088 (join_mod(root, name), true)
1089 } else if let Some(path) = resolve_extern_type(fqn, extern_paths) {
1090 (path, true)
1091 } else {
1092 (join_mod(local_module, name), false)
1093 }
1094}
1095
1096fn register_nested_types(
1106 type_map: &mut HashMap<String, String>,
1107 package_of: &mut HashMap<String, String>,
1108 package: &str,
1109 parent_fqn: &str,
1110 parent_mod: &str,
1111 msg: &crate::generated::descriptor::DescriptorProto,
1112 extern_paths: &[(String, String)],
1113) {
1114 for nested in &msg.nested_type {
1115 if let Some(name) = &nested.name {
1116 let fqn = format!("{}.{}", parent_fqn, name);
1117 let (rust_path, child_mod) = match extern_paths.iter().find(|(proto, _)| proto == &fqn)
1121 {
1122 Some((_, rust)) => {
1123 let child = match rust.rsplit_once("::") {
1124 Some((parent, _)) => format!("{parent}::{}", to_snake_case(name)),
1125 None => to_snake_case(name),
1126 };
1127 (rust.clone(), child)
1128 }
1129 None => (
1130 format!("{parent_mod}::{name}"),
1131 format!("{parent_mod}::{}", to_snake_case(name)),
1132 ),
1133 };
1134 type_map.insert(fqn.clone(), rust_path);
1135 package_of.insert(fqn.clone(), package.to_string());
1136
1137 register_nested_types(
1139 type_map,
1140 package_of,
1141 package,
1142 &fqn,
1143 &child_mod,
1144 nested,
1145 extern_paths,
1146 );
1147 }
1148 }
1149
1150 for enum_type in &msg.enum_type {
1151 if let Some(name) = &enum_type.name {
1152 let fqn = format!("{}.{}", parent_fqn, name);
1153 let rust_path = extern_paths
1154 .iter()
1155 .find(|(proto, _)| proto == &fqn)
1156 .map(|(_, rust)| rust.clone())
1157 .unwrap_or_else(|| format!("{parent_mod}::{name}"));
1158 type_map.insert(fqn.clone(), rust_path);
1159 package_of.insert(fqn, package.to_string());
1160 }
1161 }
1162}
1163
1164fn register_enum_closedness(
1166 map: &mut HashMap<String, bool>,
1167 fqn: &str,
1168 parent_features: &ResolvedFeatures,
1169 enum_desc: &EnumDescriptorProto,
1170) {
1171 let resolved = features::resolve_child(parent_features, features::enum_features(enum_desc));
1172 let closed = resolved.enum_type == features::EnumType::Closed;
1173 map.insert(fqn.to_string(), closed);
1174}
1175
1176fn register_nested_enum_closedness(
1179 map: &mut HashMap<String, bool>,
1180 parent_fqn: &str,
1181 parent_features: &ResolvedFeatures,
1182 msg: &DescriptorProto,
1183) {
1184 let msg_features = features::resolve_child(parent_features, features::message_features(msg));
1185 for enum_type in &msg.enum_type {
1186 if let Some(name) = &enum_type.name {
1187 let fqn = format!("{}.{}", parent_fqn, name);
1188 register_enum_closedness(map, &fqn, &msg_features, enum_type);
1189 }
1190 }
1191 for nested in &msg.nested_type {
1192 if let Some(name) = &nested.name {
1193 let fqn = format!("{}.{}", parent_fqn, name);
1194 register_nested_enum_closedness(map, &fqn, &msg_features, nested);
1195 }
1196 }
1197}
1198
1199#[cfg(test)]
1200mod tests {
1201 use super::*;
1202 use crate::generated::descriptor::{DescriptorProto, EnumDescriptorProto, FileDescriptorProto};
1203
1204 fn children(segs: &[&str]) -> HashSet<String> {
1205 segs.iter().map(|s| s.to_string()).collect()
1206 }
1207
1208 fn names(ns: &[&str]) -> Vec<String> {
1209 ns.iter().map(|s| s.to_string()).collect()
1210 }
1211
1212 #[test]
1213 fn deconflict_no_collision_keeps_base() {
1214 let out = deconflict_package_modules(&names(&["Oof", "Bar"]), &children(&["other"]));
1216 assert_eq!(out, vec!["oof".to_string(), "bar".to_string()]);
1217 }
1218
1219 #[test]
1220 fn deconflict_single_collision_appends_underscore() {
1221 let out = deconflict_package_modules(&names(&["Oof"]), &children(&["oof"]));
1222 assert_eq!(out, vec!["oof_".to_string()]);
1223 }
1224
1225 #[test]
1226 fn deconflict_repeated_append_when_underscore_slot_also_taken() {
1227 let out = deconflict_package_modules(&names(&["Oof"]), &children(&["oof", "oof_"]));
1229 assert_eq!(out, vec!["oof__".to_string()]);
1230 }
1231
1232 #[test]
1233 fn deconflict_two_messages_racing_to_same_slot_stay_distinct() {
1234 let out = deconflict_package_modules(&names(&["Oof", "Oof_"]), &children(&["oof", "oof_"]));
1237 assert_eq!(out, vec!["oof__".to_string(), "oof___".to_string()]);
1238 let set: HashSet<&String> = out.iter().collect();
1240 assert_eq!(set.len(), out.len());
1241 assert!(!out.contains(&"oof".to_string()) && !out.contains(&"oof_".to_string()));
1242 }
1243
1244 #[test]
1245 fn deconflict_is_independent_of_declaration_order() {
1246 let ch = children(&["oof", "oof_"]);
1248 let fwd = deconflict_package_modules(&names(&["Oof", "Oof_"]), &ch);
1249 let rev = deconflict_package_modules(&names(&["Oof_", "Oof"]), &ch);
1250 assert_eq!(fwd, vec!["oof__".to_string(), "oof___".to_string()]);
1252 assert_eq!(rev, vec!["oof___".to_string(), "oof__".to_string()]);
1253 }
1254
1255 #[test]
1256 fn deconflict_avoids_other_messages_raw_base() {
1257 let out = deconflict_package_modules(&names(&["Oof", "Oof_"]), &children(&["oof"]));
1260 assert_eq!(out, vec!["oof__".to_string(), "oof_".to_string()]);
1262 }
1263
1264 #[test]
1265 fn deconflict_never_yields_the_sentinel() {
1266 let out = deconflict_package_modules(&names(&["Buffa"]), &children(&["__buffa", "buffa"]));
1267 assert_eq!(out, vec!["buffa_".to_string()]);
1269 assert_ne!(out[0], SENTINEL_MOD);
1270 }
1271
1272 #[test]
1273 fn child_package_segments_extracts_immediate_segment() {
1274 let pkgs = children(&["foo", "foo.oof", "foo.bar.baz", "foobar"]);
1275 let mut got: Vec<String> = child_package_segments("foo", &pkgs).into_iter().collect();
1276 got.sort();
1277 assert_eq!(got, vec!["bar".to_string(), "oof".to_string()]);
1279 }
1280
1281 fn make_file(
1282 name: &str,
1283 package: &str,
1284 messages: Vec<DescriptorProto>,
1285 enums: Vec<EnumDescriptorProto>,
1286 ) -> FileDescriptorProto {
1287 FileDescriptorProto {
1288 name: Some(name.to_string()),
1289 package: if package.is_empty() {
1290 None
1291 } else {
1292 Some(package.to_string())
1293 },
1294 message_type: messages,
1295 enum_type: enums,
1296 ..Default::default()
1297 }
1298 }
1299
1300 fn msg(name: &str) -> DescriptorProto {
1301 DescriptorProto {
1302 name: Some(name.to_string()),
1303 ..Default::default()
1304 }
1305 }
1306
1307 fn msg_with_nested(name: &str, nested: Vec<DescriptorProto>) -> DescriptorProto {
1308 DescriptorProto {
1309 name: Some(name.to_string()),
1310 nested_type: nested,
1311 ..Default::default()
1312 }
1313 }
1314
1315 fn msg_with_nested_and_enums(
1316 name: &str,
1317 nested: Vec<DescriptorProto>,
1318 enums: Vec<EnumDescriptorProto>,
1319 ) -> DescriptorProto {
1320 DescriptorProto {
1321 name: Some(name.to_string()),
1322 nested_type: nested,
1323 enum_type: enums,
1324 ..Default::default()
1325 }
1326 }
1327
1328 fn enum_desc(name: &str) -> EnumDescriptorProto {
1329 EnumDescriptorProto {
1330 name: Some(name.to_string()),
1331 ..Default::default()
1332 }
1333 }
1334
1335 fn enum_with_closed_feature(name: &str) -> EnumDescriptorProto {
1336 use crate::generated::descriptor::{feature_set, EnumOptions, FeatureSet};
1337 EnumDescriptorProto {
1338 name: Some(name.to_string()),
1339 options: buffa::MessageField::some(EnumOptions {
1340 features: buffa::MessageField::some(FeatureSet {
1341 enum_type: Some(feature_set::EnumType::CLOSED),
1342 ..Default::default()
1343 }),
1344 ..Default::default()
1345 }),
1346 ..Default::default()
1347 }
1348 }
1349
1350 fn editions_file(
1351 name: &str,
1352 package: &str,
1353 messages: Vec<DescriptorProto>,
1354 enums: Vec<EnumDescriptorProto>,
1355 ) -> FileDescriptorProto {
1356 use crate::generated::descriptor::Edition;
1357 FileDescriptorProto {
1358 name: Some(name.to_string()),
1359 package: Some(package.to_string()),
1360 syntax: Some("editions".to_string()),
1361 edition: Some(Edition::EDITION_2023),
1362 message_type: messages,
1363 enum_type: enums,
1364 ..Default::default()
1365 }
1366 }
1367
1368 #[test]
1371 fn test_message_with_package() {
1372 let files = [make_file(
1373 "test.proto",
1374 "my.package",
1375 vec![msg("Foo")],
1376 vec![],
1377 )];
1378 let config = CodeGenConfig::default();
1379 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1380 assert_eq!(ctx.rust_type(".my.package.Foo"), Some("my::package::Foo"));
1381 }
1382
1383 #[test]
1384 fn test_message_no_package() {
1385 let files = [make_file("test.proto", "", vec![msg("Bar")], vec![])];
1386 let config = CodeGenConfig::default();
1387 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1388 assert_eq!(ctx.rust_type(".Bar"), Some("Bar"));
1389 }
1390
1391 #[test]
1392 fn test_nested_message_uses_module_path() {
1393 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
1394 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
1395 let config = CodeGenConfig::default();
1396 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1397 assert_eq!(ctx.rust_type(".pkg.Outer"), Some("pkg::Outer"));
1398 assert_eq!(ctx.rust_type(".pkg.Outer.Inner"), Some("pkg::outer::Inner"));
1400 }
1401
1402 #[test]
1403 fn test_nested_message_no_package() {
1404 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
1405 let files = [make_file("test.proto", "", vec![outer], vec![])];
1406 let config = CodeGenConfig::default();
1407 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1408 assert_eq!(ctx.rust_type(".Outer"), Some("Outer"));
1409 assert_eq!(ctx.rust_type(".Outer.Inner"), Some("outer::Inner"));
1410 }
1411
1412 #[test]
1413 fn test_deeply_nested_message() {
1414 let deep = msg_with_nested("A", vec![msg_with_nested("B", vec![msg("C")])]);
1415 let files = [make_file("test.proto", "pkg", vec![deep], vec![])];
1416 let config = CodeGenConfig::default();
1417 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1418 assert_eq!(ctx.rust_type(".pkg.A"), Some("pkg::A"));
1419 assert_eq!(ctx.rust_type(".pkg.A.B"), Some("pkg::a::B"));
1420 assert_eq!(ctx.rust_type(".pkg.A.B.C"), Some("pkg::a::b::C"));
1421 }
1422
1423 #[test]
1424 fn test_nested_enum_uses_module_path() {
1425 let outer = msg_with_nested_and_enums("Outer", vec![], vec![enum_desc("Status")]);
1426 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
1427 let config = CodeGenConfig::default();
1428 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1429 assert_eq!(
1430 ctx.rust_type(".pkg.Outer.Status"),
1431 Some("pkg::outer::Status")
1432 );
1433 }
1434
1435 #[test]
1436 fn test_top_level_enum() {
1437 let files = [make_file(
1438 "test.proto",
1439 "pkg",
1440 vec![],
1441 vec![enum_desc("Status")],
1442 )];
1443 let config = CodeGenConfig::default();
1444 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1445 assert_eq!(ctx.rust_type(".pkg.Status"), Some("pkg::Status"));
1446 }
1447
1448 #[test]
1449 fn test_same_named_nested_types_in_different_parents_are_distinct() {
1450 let outer1 = msg_with_nested("Outer1", vec![msg("Inner")]);
1451 let outer2 = msg_with_nested("Outer2", vec![msg("Inner")]);
1452 let files = [make_file("a.proto", "pkg", vec![outer1, outer2], vec![])];
1453 let config = CodeGenConfig::default();
1454 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1455 assert_eq!(
1457 ctx.rust_type(".pkg.Outer1.Inner"),
1458 Some("pkg::outer1::Inner")
1459 );
1460 assert_eq!(
1461 ctx.rust_type(".pkg.Outer2.Inner"),
1462 Some("pkg::outer2::Inner")
1463 );
1464 assert_ne!(
1465 ctx.rust_type(".pkg.Outer1.Inner"),
1466 ctx.rust_type(".pkg.Outer2.Inner")
1467 );
1468 }
1469
1470 #[test]
1471 fn test_multiple_files() {
1472 let files = [
1473 make_file("a.proto", "ns.a", vec![msg("MsgA")], vec![]),
1474 make_file("b.proto", "ns.b", vec![msg("MsgB")], vec![]),
1475 ];
1476 let config = CodeGenConfig::default();
1477 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1478 assert_eq!(ctx.rust_type(".ns.a.MsgA"), Some("ns::a::MsgA"));
1479 assert_eq!(ctx.rust_type(".ns.b.MsgB"), Some("ns::b::MsgB"));
1480 }
1481
1482 #[test]
1492 fn test_extern_path_exact_per_type_match() {
1493 let files = [make_file(
1494 "test.proto",
1495 "test.pkg",
1496 vec![msg("Msg")],
1497 vec![],
1498 )];
1499 let config = CodeGenConfig {
1500 extern_paths: vec![(".test.pkg.Msg".into(), "::ext_crate::Msg".into())],
1501 ..Default::default()
1502 };
1503 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1504 assert_eq!(ctx.rust_type(".test.pkg.Msg"), Some("::ext_crate::Msg"));
1506 }
1507
1508 #[test]
1509 fn test_extern_path_per_type_overrides_package_prefix() {
1510 let files = [make_file(
1514 "test.proto",
1515 "test.pkg",
1516 vec![msg("Msg"), msg("Other")],
1517 vec![],
1518 )];
1519 let config = CodeGenConfig {
1520 extern_paths: vec![
1521 (".test.pkg".into(), "::pkg_crate".into()),
1522 (".test.pkg.Msg".into(), "::ext_crate::Msg".into()),
1523 ],
1524 ..Default::default()
1525 };
1526 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1527 assert_eq!(ctx.rust_type(".test.pkg.Msg"), Some("::ext_crate::Msg"));
1528 assert_eq!(ctx.rust_type(".test.pkg.Other"), Some("::pkg_crate::Other"));
1529 }
1530
1531 #[test]
1532 fn test_extern_path_nested_type_inherits_per_type_override() {
1533 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
1536 let files = [make_file("test.proto", "test.pkg", vec![outer], vec![])];
1537 let config = CodeGenConfig {
1538 extern_paths: vec![(".test.pkg.Outer".into(), "::ext::Outer".into())],
1539 ..Default::default()
1540 };
1541 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1542 assert_eq!(ctx.rust_type(".test.pkg.Outer"), Some("::ext::Outer"));
1543 assert_eq!(
1544 ctx.rust_type(".test.pkg.Outer.Inner"),
1545 Some("::ext::outer::Inner")
1546 );
1547 }
1548
1549 #[test]
1550 fn test_extern_path_exact_per_type_enum() {
1551 let files = [make_file(
1552 "test.proto",
1553 "test.pkg",
1554 vec![],
1555 vec![enum_desc("Status")],
1556 )];
1557 let config = CodeGenConfig {
1558 extern_paths: vec![(".test.pkg.Status".into(), "::ext_crate::Status".into())],
1559 ..Default::default()
1560 };
1561 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1562 assert_eq!(
1563 ctx.rust_type(".test.pkg.Status"),
1564 Some("::ext_crate::Status")
1565 );
1566 }
1567
1568 #[test]
1569 fn test_extern_path_package_prefix_still_resolves() {
1570 let files = [make_file(
1573 "test.proto",
1574 "test.pkg",
1575 vec![msg("Msg")],
1576 vec![],
1577 )];
1578 let config = CodeGenConfig {
1579 extern_paths: vec![(".test.pkg".into(), "::pkg_crate".into())],
1580 ..Default::default()
1581 };
1582 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1583 assert_eq!(ctx.rust_type(".test.pkg.Msg"), Some("::pkg_crate::Msg"));
1584 }
1585
1586 #[test]
1587 fn test_extern_path_per_type_does_not_affect_unmapped_type() {
1588 let files = [make_file(
1590 "test.proto",
1591 "test.pkg",
1592 vec![msg("Msg"), msg("Other")],
1593 vec![],
1594 )];
1595 let config = CodeGenConfig {
1596 extern_paths: vec![(".test.pkg.Msg".into(), "::ext_crate::Msg".into())],
1597 ..Default::default()
1598 };
1599 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1600 assert_eq!(ctx.rust_type(".test.pkg.Msg"), Some("::ext_crate::Msg"));
1601 assert_eq!(ctx.rust_type(".test.pkg.Other"), Some("test::pkg::Other"));
1603 }
1604
1605 #[test]
1606 fn test_keyword_package_segment_in_type_map() {
1607 let files = [make_file(
1610 "latlng.proto",
1611 "google.type",
1612 vec![msg("LatLng")],
1613 vec![],
1614 )];
1615 let config = CodeGenConfig::default();
1616 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1617 assert_eq!(
1618 ctx.rust_type(".google.type.LatLng"),
1619 Some("google::type::LatLng")
1620 );
1621 }
1622
1623 #[test]
1624 fn test_keyword_package_relative_same_package() {
1625 let files = [make_file(
1626 "latlng.proto",
1627 "google.type",
1628 vec![msg("LatLng"), msg("Expr")],
1629 vec![],
1630 )];
1631 let config = CodeGenConfig::default();
1632 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1633 assert_eq!(
1635 ctx.rust_type_relative(".google.type.LatLng", "google.type", 0),
1636 Some("LatLng".into())
1637 );
1638 }
1639
1640 #[test]
1641 fn test_keyword_package_cross_package() {
1642 let files = [
1643 make_file("latlng.proto", "google.type", vec![msg("LatLng")], vec![]),
1644 make_file("svc.proto", "google.cloud", vec![msg("Service")], vec![]),
1645 ];
1646 let config = CodeGenConfig::default();
1647 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1648 assert_eq!(
1651 ctx.rust_type_relative(".google.type.LatLng", "google.cloud", 0),
1652 Some("super::type::LatLng".into())
1653 );
1654 }
1655
1656 #[test]
1657 fn test_keyword_nested_message_module() {
1658 let outer = msg_with_nested("Type", vec![msg("Inner")]);
1660 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
1661 let config = CodeGenConfig::default();
1662 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1663 assert_eq!(ctx.rust_type(".pkg.Type"), Some("pkg::Type"));
1664 assert_eq!(ctx.rust_type(".pkg.Type.Inner"), Some("pkg::type::Inner"));
1665 }
1666
1667 #[test]
1668 fn test_unknown_type_returns_none() {
1669 let files = [make_file("test.proto", "pkg", vec![msg("Foo")], vec![])];
1670 let config = CodeGenConfig::default();
1671 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1672 assert_eq!(ctx.rust_type(".pkg.Unknown"), None);
1673 }
1674
1675 #[test]
1678 fn test_relative_same_package_top_level() {
1679 let files = [make_file("a.proto", "pkg", vec![msg("Foo")], vec![])];
1680 let config = CodeGenConfig::default();
1681 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1682 assert_eq!(
1684 ctx.rust_type_relative(".pkg.Foo", "pkg", 0),
1685 Some("Foo".into())
1686 );
1687 }
1688
1689 #[test]
1690 fn test_relative_cross_package() {
1691 let files = [
1692 make_file("a.proto", "pkg_a", vec![msg("Foo")], vec![]),
1693 make_file("b.proto", "pkg_b", vec![msg("Bar")], vec![]),
1694 ];
1695 let config = CodeGenConfig::default();
1696 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1697 assert_eq!(
1699 ctx.rust_type_relative(".pkg_a.Foo", "pkg_b", 0),
1700 Some("super::pkg_a::Foo".into())
1701 );
1702 }
1703
1704 #[test]
1705 fn test_relative_no_package() {
1706 let files = [make_file("a.proto", "", vec![msg("Foo")], vec![])];
1707 let config = CodeGenConfig::default();
1708 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1709 assert_eq!(ctx.rust_type_relative(".Foo", "", 0), Some("Foo".into()));
1710 }
1711
1712 #[test]
1713 fn test_relative_unknown_returns_none() {
1714 let files = [make_file("a.proto", "pkg", vec![msg("Foo")], vec![])];
1715 let config = CodeGenConfig::default();
1716 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1717 assert_eq!(ctx.rust_type_relative(".pkg.Unknown", "pkg", 0), None);
1718 }
1719
1720 #[test]
1721 fn test_relative_dotted_package() {
1722 let files = [make_file("a.proto", "my.pkg", vec![msg("Foo")], vec![])];
1723 let config = CodeGenConfig::default();
1724 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1725 assert_eq!(
1726 ctx.rust_type_relative(".my.pkg.Foo", "my.pkg", 0),
1727 Some("Foo".into())
1728 );
1729 }
1730
1731 #[test]
1732 fn test_relative_cross_dotted_packages() {
1733 let files = [
1734 make_file(
1735 "timestamp.proto",
1736 "google.protobuf",
1737 vec![msg("Timestamp")],
1738 vec![],
1739 ),
1740 make_file(
1741 "test.proto",
1742 "protobuf_test_messages.proto3",
1743 vec![msg("TestAllTypesProto3")],
1744 vec![],
1745 ),
1746 ];
1747 let config = CodeGenConfig::default();
1748 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1749
1750 assert_eq!(
1752 ctx.rust_type_relative(
1753 ".google.protobuf.Timestamp",
1754 "protobuf_test_messages.proto3",
1755 0,
1756 ),
1757 Some("super::super::google::protobuf::Timestamp".into())
1758 );
1759 }
1760
1761 #[test]
1762 fn test_relative_nested_type_from_same_package() {
1763 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
1765 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
1766 let config = CodeGenConfig::default();
1767 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1768
1769 assert_eq!(
1771 ctx.rust_type_relative(".pkg.Outer.Inner", "pkg", 0),
1772 Some("outer::Inner".into())
1773 );
1774 }
1775
1776 #[test]
1777 fn test_relative_shared_prefix_not_confused() {
1778 let files = [
1779 make_file("ab.proto", "a.b", vec![msg("Msg1")], vec![]),
1780 make_file("abc.proto", "a.bc", vec![msg("Msg2")], vec![]),
1781 ];
1782 let config = CodeGenConfig::default();
1783 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1784
1785 assert_eq!(
1787 ctx.rust_type_relative(".a.b.Msg1", "a.bc", 0),
1788 Some("super::b::Msg1".into())
1789 );
1790 assert_eq!(
1792 ctx.rust_type_relative(".a.bc.Msg2", "a.b", 0),
1793 Some("super::bc::Msg2".into())
1794 );
1795 }
1796
1797 #[test]
1800 fn test_relative_cross_package_nesting_1() {
1801 let outer = msg_with_nested_and_enums("Business", vec![], vec![enum_desc("Status")]);
1805 let files = [
1806 make_file("admin.proto", "a.b.admin.v1", vec![msg("Svc")], vec![]),
1807 make_file("biz.proto", "a.b.v1", vec![outer], vec![]),
1808 ];
1809 let config = CodeGenConfig::default();
1810 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1811
1812 assert_eq!(
1814 ctx.rust_type_relative(".a.b.v1.Business.Status", "a.b.admin.v1", 0),
1815 Some("super::super::v1::business::Status".into())
1816 );
1817 assert_eq!(
1819 ctx.rust_type_relative(".a.b.v1.Business.Status", "a.b.admin.v1", 1),
1820 Some("super::super::super::v1::business::Status".into())
1821 );
1822 }
1823
1824 #[test]
1825 fn test_relative_same_package_nesting_1() {
1826 let files = [make_file(
1828 "test.proto",
1829 "pkg",
1830 vec![msg("Foo"), msg("Bar")],
1831 vec![],
1832 )];
1833 let config = CodeGenConfig::default();
1834 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1835
1836 assert_eq!(
1838 ctx.rust_type_relative(".pkg.Foo", "pkg", 0),
1839 Some("Foo".into())
1840 );
1841 assert_eq!(
1843 ctx.rust_type_relative(".pkg.Foo", "pkg", 1),
1844 Some("super::Foo".into())
1845 );
1846 assert_eq!(
1848 ctx.rust_type_relative(".pkg.Foo", "pkg", 2),
1849 Some("super::super::Foo".into())
1850 );
1851 }
1852
1853 #[test]
1856 fn test_resolve_file_extern_exact_match_only() {
1857 let mappings = [(
1858 "google/protobuf/descriptor.proto".to_string(),
1859 "::buffa_descriptor::generated::descriptor".to_string(),
1860 )];
1861 assert_eq!(
1863 resolve_file_extern("google/protobuf/descriptor.proto", &mappings),
1864 Some("::buffa_descriptor::generated::descriptor"),
1865 );
1866 assert_eq!(
1869 resolve_file_extern("google/protobuf/timestamp.proto", &mappings),
1870 None,
1871 );
1872 assert_eq!(
1874 resolve_file_extern("vendor/google/protobuf/descriptor.proto", &mappings),
1875 None,
1876 );
1877 }
1878
1879 #[test]
1880 fn test_resolve_extern_prefix_exact_match() {
1881 let result = resolve_extern_prefix(
1882 "my.common",
1883 &[(".my.common".into(), "::common_protos".into())],
1884 );
1885 assert_eq!(result, Some("::common_protos".into()));
1886 }
1887
1888 #[test]
1889 fn test_resolve_extern_prefix_sub_package() {
1890 let result = resolve_extern_prefix(
1891 "my.common.sub",
1892 &[(".my.common".into(), "::common_protos".into())],
1893 );
1894 assert_eq!(result, Some("::common_protos::sub".into()));
1895 }
1896
1897 #[test]
1898 fn test_resolve_extern_prefix_no_match() {
1899 let result = resolve_extern_prefix(
1900 "other.pkg",
1901 &[(".my.common".into(), "::common_protos".into())],
1902 );
1903 assert_eq!(result, None);
1904 }
1905
1906 #[test]
1907 fn test_resolve_extern_prefix_partial_name_no_match() {
1908 let result = resolve_extern_prefix(
1910 "my.commonext",
1911 &[(".my.common".into(), "::common_protos".into())],
1912 );
1913 assert_eq!(result, None);
1914 }
1915
1916 #[test]
1917 fn test_resolve_extern_prefix_longest_match_wins() {
1918 let result = resolve_extern_prefix(
1920 "my.common.sub",
1921 &[
1922 (".my".into(), "::crate_a".into()),
1923 (".my.common".into(), "::crate_b".into()),
1924 ],
1925 );
1926 assert_eq!(result, Some("::crate_b::sub".into()));
1927 }
1928
1929 #[test]
1930 fn test_resolve_extern_prefix_catchall() {
1931 let result = resolve_extern_prefix("greet.v1", &[(".".into(), "crate::proto".into())]);
1932 assert_eq!(result, Some("crate::proto::greet::v1".into()));
1933 }
1934
1935 #[test]
1936 fn test_resolve_extern_prefix_catchall_empty_pkg() {
1937 let result = resolve_extern_prefix("", &[(".".into(), "crate::proto".into())]);
1940 assert_eq!(result, Some("crate::proto".into()));
1941 }
1942
1943 #[test]
1944 fn test_resolve_extern_prefix_catchall_longest_wins() {
1945 let result = resolve_extern_prefix(
1948 "google.protobuf",
1949 &[
1950 (".".into(), "crate::proto".into()),
1951 (
1952 ".google.protobuf".into(),
1953 "::buffa_types::google::protobuf".into(),
1954 ),
1955 ],
1956 );
1957 assert_eq!(result, Some("::buffa_types::google::protobuf".into()));
1958 }
1959
1960 #[test]
1961 fn test_resolve_extern_prefix_catchall_keyword_package() {
1962 let result = resolve_extern_prefix("google.type", &[(".".into(), "crate::proto".into())]);
1965 assert_eq!(result, Some("crate::proto::google::type".into()));
1966 }
1967
1968 #[test]
1971 fn test_resolve_extern_type_exact_match() {
1972 let result = resolve_extern_type(
1974 ".google.protobuf.Timestamp",
1975 &[(
1976 ".google.protobuf.Timestamp".into(),
1977 "::pbjson_types::Timestamp".into(),
1978 )],
1979 );
1980 assert_eq!(result, Some("::pbjson_types::Timestamp".into()));
1981 }
1982
1983 #[test]
1984 fn test_resolve_extern_type_exact_wins_over_prefix() {
1985 let result = resolve_extern_type(
1987 ".my.pkg.Msg",
1988 &[
1989 (".my.pkg".into(), "::pkg_crate".into()),
1990 (".my.pkg.Msg".into(), "::ext_crate::Msg".into()),
1991 ],
1992 );
1993 assert_eq!(result, Some("::ext_crate::Msg".into()));
1994 }
1995
1996 #[test]
1997 fn test_resolve_extern_type_package_prefix_appends_type() {
1998 let result = resolve_extern_type(
2001 ".my.common.sub.Msg",
2002 &[(".my.common".into(), "::common_protos".into())],
2003 );
2004 assert_eq!(result, Some("::common_protos::sub::Msg".into()));
2005 }
2006
2007 #[test]
2008 fn test_resolve_extern_type_catchall() {
2009 let result = resolve_extern_type(".greet.v1.Hello", &[(".".into(), "crate::proto".into())]);
2010 assert_eq!(result, Some("crate::proto::greet::v1::Hello".into()));
2011 }
2012
2013 #[test]
2014 fn test_resolve_extern_type_no_match() {
2015 let result = resolve_extern_type(
2016 ".other.pkg.Msg",
2017 &[(".my.common".into(), "::common_protos".into())],
2018 );
2019 assert_eq!(result, None);
2020 }
2021
2022 #[test]
2023 fn test_resolve_extern_type_partial_name_no_match() {
2024 let result = resolve_extern_type(
2026 ".my.commonext.Msg",
2027 &[(".my.common".into(), "::common_protos".into())],
2028 );
2029 assert_eq!(result, None);
2030 }
2031
2032 #[test]
2035 fn test_split_extern_top_level() {
2036 let outer = msg_with_nested("Value", vec![msg("Inner")]);
2037 let files = [make_file(
2038 "struct.proto",
2039 "google.protobuf",
2040 vec![outer],
2041 vec![],
2042 )];
2043 let config = CodeGenConfig::default();
2044 let extern_paths = vec![(
2045 ".google.protobuf".into(),
2046 "::buffa_types::google::protobuf".into(),
2047 )];
2048 let ctx = CodeGenContext::new(&files, &config, &extern_paths);
2049
2050 let split = ctx
2051 .rust_type_relative_split(".google.protobuf.Value", "my.pkg", 3)
2052 .expect("type resolves");
2053 assert!(split.is_extern);
2054 assert_eq!(split.to_package, "::buffa_types::google::protobuf");
2056 assert_eq!(split.within_package, "Value");
2057 }
2058
2059 #[test]
2060 fn test_split_extern_nested_type() {
2061 let outer = msg_with_nested("Value", vec![msg("Inner")]);
2066 let files = [make_file(
2067 "struct.proto",
2068 "google.protobuf",
2069 vec![outer],
2070 vec![],
2071 )];
2072 let config = CodeGenConfig::default();
2073 let extern_paths = vec![(
2074 ".google.protobuf".into(),
2075 "::buffa_types::google::protobuf".into(),
2076 )];
2077 let ctx = CodeGenContext::new(&files, &config, &extern_paths);
2078
2079 let split = ctx
2080 .rust_type_relative_split(".google.protobuf.Value.Inner", "my.pkg", 0)
2081 .expect("nested type resolves");
2082 assert!(split.is_extern);
2083 assert_eq!(split.to_package, "::buffa_types::google::protobuf");
2084 assert_eq!(split.within_package, "value::Inner");
2085 }
2086
2087 #[test]
2088 fn test_split_per_type_extern_override() {
2089 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
2095 let files = [make_file("custom.proto", "my.pkg", vec![outer], vec![])];
2096 let config = CodeGenConfig {
2097 extern_paths: vec![(".my.pkg.Outer".into(), "::ext::custom::Outer".into())],
2098 ..Default::default()
2099 };
2100 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2101
2102 let split = ctx
2103 .rust_type_relative_split(".my.pkg.Outer", "other.pkg", 2)
2104 .expect("overridden type resolves");
2105 assert!(split.is_extern);
2106 assert_eq!(split.to_package, "::ext::custom");
2107 assert_eq!(split.within_package, "Outer");
2108
2109 let nested = ctx
2112 .rust_type_relative_split(".my.pkg.Outer.Inner", "other.pkg", 0)
2113 .expect("nested type resolves");
2114 assert!(nested.is_extern);
2115 assert_eq!(nested.to_package, "::ext::custom");
2116 assert_eq!(nested.within_package, "outer::Inner");
2117 }
2118
2119 #[test]
2120 fn test_extern_path_top_level_message() {
2121 let files = [make_file(
2122 "common.proto",
2123 "my.common",
2124 vec![msg("SharedMsg")],
2125 vec![],
2126 )];
2127 let config = CodeGenConfig {
2128 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
2129 ..Default::default()
2130 };
2131 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2132 assert_eq!(
2133 ctx.rust_type(".my.common.SharedMsg"),
2134 Some("::common_protos::SharedMsg")
2135 );
2136 }
2137
2138 #[test]
2139 fn test_extern_path_nested_message() {
2140 let files = [make_file(
2141 "common.proto",
2142 "my.common",
2143 vec![msg_with_nested("Outer", vec![msg("Inner")])],
2144 vec![],
2145 )];
2146 let config = CodeGenConfig {
2147 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
2148 ..Default::default()
2149 };
2150 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2151 assert_eq!(
2152 ctx.rust_type(".my.common.Outer"),
2153 Some("::common_protos::Outer")
2154 );
2155 assert_eq!(
2156 ctx.rust_type(".my.common.Outer.Inner"),
2157 Some("::common_protos::outer::Inner")
2158 );
2159 }
2160
2161 #[test]
2162 fn test_extern_path_enum() {
2163 let files = [make_file(
2164 "common.proto",
2165 "my.common",
2166 vec![],
2167 vec![enum_desc("Status")],
2168 )];
2169 let config = CodeGenConfig {
2170 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
2171 ..Default::default()
2172 };
2173 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2174 assert_eq!(
2175 ctx.rust_type(".my.common.Status"),
2176 Some("::common_protos::Status")
2177 );
2178 }
2179
2180 #[test]
2181 fn test_extern_path_does_not_affect_other_packages() {
2182 let files = [
2183 make_file("common.proto", "my.common", vec![msg("SharedMsg")], vec![]),
2184 make_file(
2185 "service.proto",
2186 "my.service",
2187 vec![msg("MyService")],
2188 vec![],
2189 ),
2190 ];
2191 let config = CodeGenConfig {
2192 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
2193 ..Default::default()
2194 };
2195 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2196 assert_eq!(
2198 ctx.rust_type(".my.common.SharedMsg"),
2199 Some("::common_protos::SharedMsg")
2200 );
2201 assert_eq!(
2203 ctx.rust_type(".my.service.MyService"),
2204 Some("my::service::MyService")
2205 );
2206 }
2207
2208 #[test]
2209 fn test_extern_path_relative_returns_absolute() {
2210 let files = [
2213 make_file("common.proto", "my.common", vec![msg("SharedMsg")], vec![]),
2214 make_file(
2215 "service.proto",
2216 "my.service",
2217 vec![msg("MyService")],
2218 vec![],
2219 ),
2220 ];
2221 let config = CodeGenConfig {
2222 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
2223 ..Default::default()
2224 };
2225 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2226 assert_eq!(
2228 ctx.rust_type_relative(".my.common.SharedMsg", "my.service", 0),
2229 Some("::common_protos::SharedMsg".into())
2230 );
2231 }
2232
2233 #[test]
2236 fn test_is_enum_closed_proto3_default_open() {
2237 let files = [make_file("a.proto", "p", vec![], vec![enum_desc("E")])];
2238 let config = CodeGenConfig::default();
2239 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2240 assert_eq!(ctx.is_enum_closed(".p.E"), Some(true));
2244 }
2245
2246 #[test]
2247 fn test_is_enum_closed_editions_default_open() {
2248 let files = [editions_file("a.proto", "p", vec![], vec![enum_desc("E")])];
2249 let config = CodeGenConfig::default();
2250 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2251 assert_eq!(ctx.is_enum_closed(".p.E"), Some(false));
2253 }
2254
2255 #[test]
2256 fn test_is_enum_closed_per_enum_override() {
2257 let files = [editions_file(
2260 "a.proto",
2261 "p",
2262 vec![],
2263 vec![enum_desc("Open"), enum_with_closed_feature("Closed")],
2264 )];
2265 let config = CodeGenConfig::default();
2266 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2267 assert_eq!(ctx.is_enum_closed(".p.Open"), Some(false));
2268 assert_eq!(ctx.is_enum_closed(".p.Closed"), Some(true));
2269 }
2270
2271 #[test]
2272 fn test_is_enum_closed_nested_per_enum_override() {
2273 let files = [editions_file(
2275 "a.proto",
2276 "p",
2277 vec![msg_with_nested_and_enums(
2278 "M",
2279 vec![],
2280 vec![enum_with_closed_feature("Inner")],
2281 )],
2282 vec![],
2283 )];
2284 let config = CodeGenConfig::default();
2285 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2286 assert_eq!(ctx.is_enum_closed(".p.M.Inner"), Some(true));
2287 }
2288
2289 #[test]
2290 fn test_is_enum_closed_unknown_enum_returns_none() {
2291 let files = [editions_file("a.proto", "p", vec![], vec![])];
2292 let config = CodeGenConfig::default();
2293 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2294 assert_eq!(ctx.is_enum_closed(".other.Unknown"), None);
2296 }
2297
2298 #[test]
2299 fn test_for_generate_auto_injects_wkt_mapping() {
2300 let ts_msg = DescriptorProto {
2303 name: Some("Timestamp".into()),
2304 ..Default::default()
2305 };
2306 let files = [FileDescriptorProto {
2307 name: Some("google/protobuf/timestamp.proto".into()),
2308 package: Some("google.protobuf".into()),
2309 syntax: Some("proto3".into()),
2310 message_type: vec![ts_msg],
2311 ..Default::default()
2312 }];
2313 let config = CodeGenConfig::default();
2314 let ctx = CodeGenContext::for_generate(&files, &["other.proto".into()], &config);
2316 assert_eq!(
2317 ctx.rust_type(".google.protobuf.Timestamp"),
2318 Some("::buffa_types::google::protobuf::Timestamp"),
2319 "WKT auto-mapping must be applied via for_generate"
2320 );
2321 }
2322
2323 #[test]
2324 fn test_for_generate_suppresses_wkt_when_generating_wkt() {
2325 let ts_msg = DescriptorProto {
2328 name: Some("Timestamp".into()),
2329 ..Default::default()
2330 };
2331 let files = [FileDescriptorProto {
2332 name: Some("google/protobuf/timestamp.proto".into()),
2333 package: Some("google.protobuf".into()),
2334 syntax: Some("proto3".into()),
2335 message_type: vec![ts_msg],
2336 ..Default::default()
2337 }];
2338 let config = CodeGenConfig::default();
2339 let ctx = CodeGenContext::for_generate(
2340 &files,
2341 &["google/protobuf/timestamp.proto".into()],
2342 &config,
2343 );
2344 assert_eq!(
2346 ctx.rust_type(".google.protobuf.Timestamp"),
2347 Some("google::protobuf::Timestamp")
2348 );
2349 }
2350
2351 #[test]
2354 fn test_matching_attributes_catchall() {
2355 let attrs = vec![(".".into(), "#[derive(Foo)]".into())];
2357 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
2358 assert!(result.to_string().contains("derive"));
2359 }
2360
2361 #[test]
2362 fn test_matching_attributes_exact_match() {
2363 let attrs = vec![(".my.pkg.MyMessage".into(), "#[derive(Bar)]".into())];
2364 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
2365 assert!(result.to_string().contains("derive"));
2366 }
2367
2368 #[test]
2369 fn test_matching_attributes_package_prefix() {
2370 let attrs = vec![(".my.pkg".into(), "#[derive(Baz)]".into())];
2371 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
2372 assert!(result.to_string().contains("derive"));
2373 }
2374
2375 #[test]
2376 fn test_matching_attributes_no_partial_segment_match() {
2377 let attrs = vec![(".my.pk".into(), "#[derive(Bad)]".into())];
2379 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
2380 assert!(result.is_empty());
2381 }
2382
2383 #[test]
2384 fn test_matching_attributes_no_match() {
2385 let attrs = vec![(".other.pkg".into(), "#[derive(Nope)]".into())];
2386 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
2387 assert!(result.is_empty());
2388 }
2389
2390 #[test]
2391 fn test_matching_attributes_multiple_accumulate() {
2392 let attrs = vec![
2394 (".".into(), "#[derive(A)]".into()),
2395 (".my.pkg".into(), "#[derive(B)]".into()),
2396 ];
2397 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
2398 let s = result.to_string();
2399 assert!(s.contains("A") && s.contains("B"));
2400 }
2401
2402 #[test]
2403 fn test_matching_attributes_invalid_attr_errors() {
2404 let attrs = vec![(".".into(), "not valid {{{{".into())];
2407 let err = CodeGenContext::matching_attributes(&attrs, "my.pkg.Msg").unwrap_err();
2408 assert!(matches!(
2409 err,
2410 crate::CodeGenError::InvalidCustomAttribute { .. }
2411 ));
2412 }
2413
2414 #[test]
2415 fn test_matches_proto_prefix_catchall() {
2416 assert!(matches_proto_prefix(".", ".anything.here"));
2417 assert!(matches_proto_prefix(".", "."));
2418 }
2419
2420 #[test]
2421 fn test_matches_proto_prefix_segment_boundary() {
2422 assert!(!matches_proto_prefix(".my.pk", ".my.pkg.Msg"));
2424 assert!(matches_proto_prefix(".my.pkg", ".my.pkg.Msg"));
2426 }
2427}