1use std::borrow::Cow;
4use std::collections::{HashMap, HashSet};
5
6use crate::features::{self, ResolvedFeatures};
7use crate::generated::descriptor::{DescriptorProto, EnumDescriptorProto, FileDescriptorProto};
8use crate::oneof::to_snake_case;
9use crate::CodeGenConfig;
10
11pub const SENTINEL_MOD: &str = "__buffa";
18
19#[derive(Debug, Clone)]
25pub struct SplitPath {
26 pub to_package: String,
34 pub within_package: String,
37 pub is_extern: bool,
40}
41
42pub struct CodeGenContext<'a> {
48 pub files: &'a [FileDescriptorProto],
50 pub config: &'a CodeGenConfig,
52 pub type_map: HashMap<String, String>,
58 package_of: HashMap<String, String>,
63 enum_closedness: HashMap<String, bool>,
71 enum_first_value: HashMap<String, i32>,
77 comment_map: HashMap<String, String>,
90 nested_module_names: HashMap<String, String>,
100 unboxed_oneof_variants: HashSet<String>,
105 inlined_message_fields: HashSet<String>,
111 warnings: std::cell::RefCell<Vec<crate::CodeGenWarning>>,
123 field_renames: HashMap<(String, i32), String>,
129 oneof_keep_verbatim: HashSet<String>,
132 imports: std::cell::RefCell<crate::imports::ImportsPhase>,
144}
145
146fn child_package_segments(package: &str, all_packages: &HashSet<String>) -> HashSet<String> {
153 let prefix = if package.is_empty() {
154 String::new()
155 } else {
156 format!("{package}.")
157 };
158 all_packages
159 .iter()
160 .filter_map(|p| {
161 let rest = if package.is_empty() {
162 Some(p.as_str())
163 } else {
164 p.strip_prefix(&prefix)
165 };
166 rest.filter(|r| !r.is_empty())
167 .map(|r| r.split('.').next().unwrap_or(r).to_string())
168 })
169 .collect()
170}
171
172fn deconflict_package_modules(message_names: &[String], children: &HashSet<String>) -> Vec<String> {
192 let bases: Vec<String> = message_names.iter().map(|n| to_snake_case(n)).collect();
193 let mut taken: HashSet<String> = children.clone();
196 taken.insert(SENTINEL_MOD.to_string());
197 taken.extend(bases.iter().cloned());
198
199 let mut out = bases.clone();
204 let mut order: Vec<usize> = (0..bases.len()).collect();
205 order.sort_by(|&a, &b| bases[a].cmp(&bases[b]));
206 for i in order {
207 if !children.contains(&bases[i]) {
208 continue;
209 }
210 let mut candidate = format!("{}_", bases[i]);
211 while taken.contains(&candidate) {
212 candidate.push('_');
213 }
214 taken.insert(candidate.clone());
215 out[i] = candidate;
216 }
217 out
218}
219
220impl<'a> CodeGenContext<'a> {
221 pub fn new(
233 files: &'a [FileDescriptorProto],
234 config: &'a CodeGenConfig,
235 effective_extern_paths: &[(String, String)],
236 ) -> Self {
237 Self::with_extern_resolution(files, config, effective_extern_paths, &[])
238 }
239
240 pub(crate) fn with_extern_resolution(
263 files: &'a [FileDescriptorProto],
264 config: &'a CodeGenConfig,
265 effective_extern_paths: &[(String, String)],
266 file_extern_paths: &[(String, String)],
267 ) -> Self {
268 let mut type_map = HashMap::new();
269 let mut package_of = HashMap::new();
270 let mut enum_closedness = HashMap::new();
271 let mut comment_map = HashMap::new();
272 let mut nested_module_names = HashMap::new();
273 let msg_index = crate::oneof::message_index(files);
274 let unboxed_oneof_variants = crate::oneof::resolve_unboxed_variants(
275 &msg_index,
276 &config.unboxed_oneof_fields,
277 &config.pointer_fields,
278 );
279 let inlined_message_fields = crate::oneof::resolve_inlined_fields(
280 &msg_index,
281 &config.unboxed_oneof_fields,
282 &config.pointer_fields,
283 );
284
285 let mut all_packages: HashSet<String> = HashSet::new();
292 let mut pkg_message_names: HashMap<String, Vec<String>> = HashMap::new();
293 for file in files {
294 let package = file.package.as_deref().unwrap_or("");
295 all_packages.insert(package.to_string());
296 for msg in &file.message_type {
297 if let Some(name) = &msg.name {
298 let fqn = if package.is_empty() {
302 format!(".{name}")
303 } else {
304 format!(".{package}.{name}")
305 };
306 if effective_extern_paths
307 .iter()
308 .any(|(proto, _)| proto == &fqn)
309 {
310 continue;
311 }
312 pkg_message_names
313 .entry(package.to_string())
314 .or_default()
315 .push(name.clone());
316 }
317 }
318 }
319
320 for (package, names) in &pkg_message_names {
325 let children = child_package_segments(package, &all_packages);
326 let modules = deconflict_package_modules(names, &children);
327 for (name, module) in names.iter().zip(modules) {
328 let fqn = if package.is_empty() {
329 format!(".{name}")
330 } else {
331 format!(".{package}.{name}")
332 };
333 nested_module_names.insert(fqn, module);
334 }
335 }
336
337 for file in files {
338 comment_map.extend(crate::comments::fqn_comments(file));
339 let package = file.package.as_deref().unwrap_or("");
340 let file_features = features::for_file(file);
341 let proto_prefix = if package.is_empty() {
342 String::from(".")
343 } else {
344 format!(".{}.", package)
345 };
346
347 let file_root = file
357 .name
358 .as_deref()
359 .and_then(|n| resolve_file_extern(n, file_extern_paths));
360 let local_module = package.replace('.', "::");
361
362 for msg in &file.message_type {
364 if let Some(name) = &msg.name {
365 let fqn = format!("{}{}", proto_prefix, name);
366 let (rust_path, is_extern) = resolve_type_path(
367 &fqn,
368 name,
369 file_root,
370 &local_module,
371 effective_extern_paths,
372 &config.type_name_prefix,
373 );
374
375 let parent_mod = if is_extern {
383 let snake = nested_module_names
384 .get(&fqn)
385 .cloned()
386 .unwrap_or_else(|| to_snake_case(name));
387 match rust_path.rsplit_once("::") {
388 Some((parent, _)) => format!("{parent}::{snake}"),
389 None => snake,
390 }
391 } else {
392 let snake = nested_module_names
393 .get(&fqn)
394 .cloned()
395 .unwrap_or_else(|| to_snake_case(name));
396 join_mod(&local_module, &snake)
397 };
398
399 type_map.insert(fqn.clone(), rust_path);
400 package_of.insert(fqn.clone(), package.to_string());
401 register_nested_types(
402 &mut type_map,
403 &mut package_of,
404 NestedRegistrationCtx {
405 package,
406 extern_paths: effective_extern_paths,
407 type_name_prefix: &config.type_name_prefix,
408 },
409 &fqn,
410 &parent_mod,
411 msg,
412 );
413 register_nested_enum_closedness(
414 &mut enum_closedness,
415 &fqn,
416 &file_features,
417 msg,
418 );
419 }
420 }
421
422 for enum_type in &file.enum_type {
424 if let Some(name) = &enum_type.name {
425 let fqn = format!("{}{}", proto_prefix, name);
426 let (rust_path, _) = resolve_type_path(
427 &fqn,
428 name,
429 file_root,
430 &local_module,
431 effective_extern_paths,
432 &config.type_name_prefix,
433 );
434 type_map.insert(fqn.clone(), rust_path);
435 package_of.insert(fqn.clone(), package.to_string());
436 register_enum_closedness(&mut enum_closedness, &fqn, &file_features, enum_type);
437 }
438 }
439 }
440
441 let (field_renames, oneof_keep_verbatim, plan_warnings) = if config.idiomatic_field_names {
446 let plan = crate::field_names::plan_field_names(files);
447 (plan.field_renames, plan.oneof_keep_verbatim, plan.warnings)
448 } else {
449 (HashMap::new(), HashSet::new(), Vec::new())
450 };
451
452 Self {
453 files,
454 config,
455 type_map,
456 package_of,
457 enum_closedness,
458 enum_first_value: if config.has_enum_type_overrides() {
462 collect_enum_first_values(files)
463 } else {
464 HashMap::new()
465 },
466 comment_map,
467 nested_module_names,
468 unboxed_oneof_variants,
469 inlined_message_fields,
470 field_renames,
471 oneof_keep_verbatim,
472 warnings: std::cell::RefCell::new(plan_warnings),
473 imports: std::cell::RefCell::new(crate::imports::ImportsPhase::Off),
474 }
475 }
476
477 pub(crate) fn field_rust_name<'n>(&'n self, name: &'n str, number: i32) -> Cow<'n, str> {
484 if !self.config.idiomatic_field_names {
485 return Cow::Borrowed(name);
486 }
487 if !self.field_renames.is_empty() {
488 if let Some(renamed) = self.field_renames.get(&(name.to_string(), number)) {
489 return Cow::Borrowed(renamed.as_str());
490 }
491 }
492 let converted = crate::field_names::idiomatic_snake_case(name);
493 if converted == name {
494 Cow::Borrowed(name)
495 } else {
496 Cow::Owned(converted)
497 }
498 }
499
500 pub(crate) fn field_ident(&self, name: &str, number: i32) -> proc_macro2::Ident {
503 crate::idents::make_field_ident(&self.field_rust_name(name, number))
504 }
505
506 pub(crate) fn oneof_rust_name<'n>(&'n self, name: &'n str) -> Cow<'n, str> {
511 if !self.config.idiomatic_field_names || self.oneof_keep_verbatim.contains(name) {
512 return Cow::Borrowed(name);
513 }
514 let converted = crate::field_names::idiomatic_snake_case(name);
515 if converted == name {
516 Cow::Borrowed(name)
517 } else {
518 Cow::Owned(converted)
519 }
520 }
521
522 pub(crate) fn oneof_ident(&self, name: &str) -> proc_macro2::Ident {
525 crate::idents::make_field_ident(&self.oneof_rust_name(name))
526 }
527
528 pub(crate) fn field_rename_note(&self, name: &str, number: i32) -> Option<String> {
533 if !self.config.idiomatic_field_names || self.field_renames.is_empty() {
534 return None;
535 }
536 let resolved = self.field_renames.get(&(name.to_string(), number))?;
537 Some(format!(
538 " Note: the snake_case conversion of `{name}` collides with another \
539 member of this message; the Rust name was adjusted to `{resolved}` \
540 (`_f<n>` suffixes carry the field number)."
541 ))
542 }
543
544 pub(crate) fn message_non_snake_attr(&self, msg: &DescriptorProto) -> proc_macro2::TokenStream {
558 let non_snake = |s: &str| s.contains(|c: char| c.is_ascii_uppercase());
559 let mut real_oneofs: HashSet<i32> = HashSet::new();
560 let mut found = false;
561 for field in &msg.field {
562 let Some(name) = field.name.as_deref() else {
563 continue;
564 };
565 if field.oneof_index.is_some() && !field.proto3_optional.unwrap_or(false) {
568 if let Some(idx) = field.oneof_index {
569 real_oneofs.insert(idx);
570 }
571 continue;
572 }
573 if non_snake(&self.field_rust_name(name, field.number.unwrap_or(0))) {
574 found = true;
575 break;
576 }
577 }
578 if !found {
579 for (idx, oneof) in msg.oneof_decl.iter().enumerate() {
580 let Some(name) = oneof.name.as_deref() else {
581 continue;
582 };
583 if !real_oneofs.contains(&i32::try_from(idx).unwrap_or(i32::MAX)) {
584 continue;
585 }
586 if non_snake(&self.oneof_rust_name(name)) {
587 found = true;
588 break;
589 }
590 }
591 }
592 if found {
593 quote::quote! { #[allow(non_snake_case)] }
594 } else {
595 proc_macro2::TokenStream::new()
596 }
597 }
598
599 pub(crate) fn warn(&self, warning: crate::CodeGenWarning) {
601 self.warnings.borrow_mut().push(warning);
602 }
603
604 pub(crate) fn take_warnings(&self) -> Vec<crate::CodeGenWarning> {
610 self.warnings.take()
611 }
612
613 pub(crate) fn truncate_warnings(&self, len: usize) {
618 self.warnings.borrow_mut().truncate(len);
619 }
620
621 pub(crate) fn warnings_len(&self) -> usize {
624 self.warnings.borrow().len()
625 }
626
627 pub(crate) fn imports_begin_collecting(&self) {
632 *self.imports.borrow_mut() =
633 crate::imports::ImportsPhase::Collecting(std::collections::BTreeSet::new());
634 }
635
636 pub(crate) fn imports_take_collected(&self) -> std::collections::BTreeSet<String> {
638 match self.imports.replace(crate::imports::ImportsPhase::Off) {
639 crate::imports::ImportsPhase::Collecting(set) => set,
640 _ => std::collections::BTreeSet::new(),
641 }
642 }
643
644 pub(crate) fn imports_set_resolving(&self, imports: crate::imports::RootImports) {
646 *self.imports.borrow_mut() = crate::imports::ImportsPhase::Resolving(imports);
647 }
648
649 pub(crate) fn imports_reset(&self) {
654 *self.imports.borrow_mut() = crate::imports::ImportsPhase::Off;
655 }
656
657 pub(crate) fn imports_use_block(&self) -> proc_macro2::TokenStream {
660 match &*self.imports.borrow() {
661 crate::imports::ImportsPhase::Resolving(imports) => imports.use_items(),
662 _ => proc_macro2::TokenStream::new(),
663 }
664 }
665
666 fn root_path(&self, path: String, nesting: usize) -> String {
673 if nesting != 0 {
674 return path;
675 }
676 match &mut *self.imports.borrow_mut() {
677 crate::imports::ImportsPhase::Off => path,
678 crate::imports::ImportsPhase::Collecting(set) => {
679 if crate::imports::shortenable(&path) {
680 set.insert(path.clone());
681 }
682 path
683 }
684 crate::imports::ImportsPhase::Resolving(imports) => match imports.resolve(&path) {
685 Some(short) => short.to_string(),
686 None => path,
687 },
688 }
689 }
690
691 pub(crate) fn root_runtime_path(
697 &self,
698 canonical: &str,
699 nesting: usize,
700 ) -> proc_macro2::TokenStream {
701 crate::idents::rust_path_to_tokens(&self.root_path(canonical.to_string(), nesting))
702 }
703
704 pub fn nested_module_name(&self, package: &str, name: &str) -> String {
713 let fqn = if package.is_empty() {
714 format!(".{name}")
715 } else {
716 format!(".{package}.{name}")
717 };
718 self.nested_module_names
719 .get(&fqn)
720 .cloned()
721 .unwrap_or_else(|| to_snake_case(name))
722 }
723
724 pub fn for_generate(
738 files: &'a [FileDescriptorProto],
739 files_to_generate: &[String],
740 config: &'a CodeGenConfig,
741 ) -> Self {
742 let paths = crate::effective_extern_paths(files, files_to_generate, config);
743 let file_paths = crate::effective_file_extern_paths(files_to_generate, config);
744 Self::with_extern_resolution(files, config, &paths, &file_paths)
745 }
746
747 pub fn rust_type(&self, proto_fqn: &str) -> Option<&str> {
749 self.type_map.get(proto_fqn).map(|s| s.as_str())
750 }
751
752 pub fn comment(&self, fqn: &str) -> Option<&str> {
761 self.comment_map.get(fqn).map(|s| s.as_str())
762 }
763
764 pub fn is_enum_closed(&self, proto_fqn: &str) -> Option<bool> {
771 self.enum_closedness.get(proto_fqn).copied()
772 }
773
774 pub(crate) fn enum_first_value(&self, proto_fqn: &str) -> Option<i32> {
777 self.enum_first_value.get(proto_fqn).copied()
778 }
779
780 pub fn rust_type_relative(
795 &self,
796 proto_fqn: &str,
797 current_package: &str,
798 nesting: usize,
799 ) -> Option<String> {
800 let full_path = self.type_map.get(proto_fqn)?;
801
802 if full_path.starts_with("::") || full_path.starts_with("crate::") {
805 return Some(self.root_path(full_path.clone(), nesting));
806 }
807
808 let target_package = self
809 .package_of
810 .get(proto_fqn)
811 .map(|s| s.as_str())
812 .unwrap_or("");
813
814 let target_rust_module = target_package.replace('.', "::");
817 let type_suffix = if target_rust_module.is_empty() {
818 full_path.as_str()
819 } else {
820 full_path
821 .strip_prefix(&format!("{}::", target_rust_module))
822 .unwrap_or(full_path)
823 };
824
825 if current_package == target_package {
826 if nesting == 0 {
828 return Some(type_suffix.to_string());
829 }
830 let supers = (0..nesting).map(|_| "super").collect::<Vec<_>>().join("::");
831 return Some(format!("{}::{}", supers, type_suffix));
832 }
833
834 let current_parts: Vec<&str> = if current_package.is_empty() {
836 vec![]
837 } else {
838 current_package.split('.').collect()
839 };
840 let target_parts: Vec<&str> = if target_package.is_empty() {
841 vec![]
842 } else {
843 target_package.split('.').collect()
844 };
845
846 let common_len = current_parts
848 .iter()
849 .zip(&target_parts)
850 .take_while(|(a, b)| a == b)
851 .count();
852
853 let up_count = (current_parts.len() - common_len) + nesting;
856
857 let down_parts = &target_parts[common_len..];
859
860 let mut segments: Vec<&str> = vec!["super"; up_count];
861 segments.extend_from_slice(down_parts);
862
863 let mut result = segments.join("::");
865 if !result.is_empty() {
866 result.push_str("::");
867 }
868 result.push_str(type_suffix);
869
870 Some(self.root_path(result, nesting))
871 }
872
873 pub fn rust_type_relative_split(
885 &self,
886 proto_fqn: &str,
887 current_package: &str,
888 nesting: usize,
889 ) -> Option<SplitPath> {
890 let full_path = self.type_map.get(proto_fqn)?;
891
892 let target_package = self
893 .package_of
894 .get(proto_fqn)
895 .map(|s| s.as_str())
896 .unwrap_or("");
897
898 let target_rust_module = if full_path.starts_with("::") || full_path.starts_with("crate::")
904 {
905 let fqn_no_dot = proto_fqn.strip_prefix('.').unwrap_or(proto_fqn);
914 let within_proto = if target_package.is_empty() {
915 fqn_no_dot
916 } else {
917 fqn_no_dot
918 .strip_prefix(target_package)
919 .and_then(|s| s.strip_prefix('.'))
920 .unwrap_or(fqn_no_dot)
921 };
922 let within_segs = within_proto.split('.').count();
936 let full_segs: Vec<&str> = full_path.split("::").collect();
937 let cut = full_segs.len().saturating_sub(within_segs);
938 full_segs[..cut].join("::")
939 } else {
940 target_package.replace('.', "::")
941 };
942
943 let type_suffix = if target_rust_module.is_empty() {
944 full_path.as_str()
945 } else {
946 full_path
947 .strip_prefix(&format!("{}::", target_rust_module))
948 .unwrap_or(full_path)
949 };
950
951 if full_path.starts_with("::") || full_path.starts_with("crate::") {
953 return Some(SplitPath {
954 to_package: target_rust_module,
955 within_package: type_suffix.to_string(),
956 is_extern: true,
957 });
958 }
959
960 if current_package == target_package {
961 let to_package = if nesting == 0 {
962 String::new()
963 } else {
964 (0..nesting).map(|_| "super").collect::<Vec<_>>().join("::")
965 };
966 return Some(SplitPath {
967 to_package,
968 within_package: type_suffix.to_string(),
969 is_extern: false,
970 });
971 }
972
973 let current_parts: Vec<&str> = if current_package.is_empty() {
975 vec![]
976 } else {
977 current_package.split('.').collect()
978 };
979 let target_parts: Vec<&str> = if target_package.is_empty() {
980 vec![]
981 } else {
982 target_package.split('.').collect()
983 };
984 let common_len = current_parts
985 .iter()
986 .zip(&target_parts)
987 .take_while(|(a, b)| a == b)
988 .count();
989 let up_count = (current_parts.len() - common_len) + nesting;
990 let down_parts = &target_parts[common_len..];
991
992 let mut segments: Vec<&str> = vec!["super"; up_count];
993 segments.extend_from_slice(down_parts);
994
995 Some(SplitPath {
996 to_package: segments.join("::"),
997 within_package: type_suffix.to_string(),
998 is_extern: false,
999 })
1000 }
1001
1002 pub(crate) fn matching_attributes(
1015 attrs: &[(String, String)],
1016 fqn: &str,
1017 ) -> Result<proc_macro2::TokenStream, crate::CodeGenError> {
1018 if attrs.is_empty() {
1019 return Ok(proc_macro2::TokenStream::new());
1020 }
1021 let fqn_dotted = format!(".{fqn}");
1022 let mut tokens = proc_macro2::TokenStream::new();
1023 for (prefix, attr_str) in attrs {
1024 if matches_proto_prefix(prefix, &fqn_dotted) {
1025 let parsed =
1026 syn::parse_str::<proc_macro2::TokenStream>(attr_str).map_err(|err| {
1027 crate::CodeGenError::InvalidCustomAttribute {
1028 path: prefix.clone(),
1029 attribute: attr_str.clone(),
1030 detail: err.to_string(),
1031 }
1032 })?;
1033 tokens.extend(parsed);
1034 }
1035 }
1036 Ok(tokens)
1037 }
1038
1039 pub fn bytes_repr(&self, field_fqn: &str) -> crate::BytesRepr {
1050 self.config
1051 .bytes_fields
1052 .iter()
1053 .rev()
1054 .find(|(prefix, _)| matches_proto_prefix(prefix, field_fqn))
1055 .map_or(crate::BytesRepr::default(), |(_, repr)| repr.clone())
1056 }
1057
1058 pub fn oneof_unboxed(&self, variant_fqn: &str) -> bool {
1068 self.unboxed_oneof_variants.contains(variant_fqn)
1069 }
1070
1071 pub fn string_repr(&self, field_fqn: &str) -> crate::StringRepr {
1081 self.config
1082 .string_fields
1083 .iter()
1084 .rev()
1085 .find(|(prefix, _)| matches_proto_prefix(prefix, field_fqn))
1086 .map_or(crate::StringRepr::default(), |(_, repr)| repr.clone())
1087 }
1088
1089 pub fn map_repr(&self, field_fqn: &str) -> crate::MapRepr {
1099 self.config
1100 .map_fields
1101 .iter()
1102 .rev()
1103 .find(|(prefix, _)| matches_proto_prefix(prefix, field_fqn))
1104 .map_or(crate::MapRepr::default(), |(_, repr)| repr.clone())
1105 }
1106
1107 pub fn pointer_repr(&self, field_fqn: &str) -> crate::PointerRepr {
1118 let raw = self
1119 .config
1120 .pointer_fields
1121 .iter()
1122 .rev()
1123 .find(|(prefix, _)| matches_proto_prefix(prefix, field_fqn))
1124 .map_or(crate::PointerRepr::default(), |(_, repr)| repr.clone());
1125 if raw == crate::PointerRepr::Inline && !self.inlined_message_fields.contains(field_fqn) {
1126 crate::PointerRepr::Box
1127 } else {
1128 raw
1129 }
1130 }
1131
1132 pub fn repeated_repr(&self, field_fqn: &str) -> crate::RepeatedRepr {
1142 self.config
1143 .repeated_fields
1144 .iter()
1145 .rev()
1146 .find(|(prefix, _)| matches_proto_prefix(prefix, field_fqn))
1147 .map_or(crate::RepeatedRepr::default(), |(_, repr)| repr.clone())
1148 }
1149}
1150
1151#[derive(Clone, Copy)]
1158pub(crate) struct MessageScope<'a> {
1159 pub ctx: &'a CodeGenContext<'a>,
1161 pub current_package: &'a str,
1163 pub proto_fqn: &'a str,
1166 pub features: &'a ResolvedFeatures,
1168 pub nesting: usize,
1172}
1173
1174impl<'a> MessageScope<'a> {
1175 pub fn nested(&self, proto_fqn: &'a str, features: &'a ResolvedFeatures) -> MessageScope<'a> {
1177 MessageScope {
1178 ctx: self.ctx,
1179 current_package: self.current_package,
1180 proto_fqn,
1181 features,
1182 nesting: self.nesting + 1,
1183 }
1184 }
1185}
1186
1187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1192pub(crate) enum AncillaryKind {
1193 Oneof,
1195 View,
1197 ViewOneof,
1199 LazyView,
1201}
1202
1203impl AncillaryKind {
1204 fn path_segments(self) -> &'static [&'static str] {
1205 match self {
1206 Self::Oneof => &["oneof"],
1207 Self::View => &["view"],
1208 Self::ViewOneof => &["view", "oneof"],
1209 Self::LazyView => &["lazy_view"],
1210 }
1211 }
1212}
1213
1214pub(crate) fn ancillary_prefix(
1229 kind: AncillaryKind,
1230 current_package: &str,
1231 proto_fqn: &str,
1232 from_nesting: usize,
1233) -> proc_macro2::TokenStream {
1234 use crate::idents::make_field_ident;
1235 use quote::quote;
1236
1237 debug_assert!(
1238 !proto_fqn.starts_with('.'),
1239 "ancillary_prefix expects dotless FQN, got {proto_fqn:?}"
1240 );
1241
1242 let mut supers_tokens = proc_macro2::TokenStream::new();
1243 for _ in 0..from_nesting {
1244 supers_tokens.extend(quote! { super:: });
1245 }
1246
1247 let sentinel = make_field_ident(SENTINEL_MOD);
1248 let kind_segs: Vec<_> = kind
1249 .path_segments()
1250 .iter()
1251 .map(|s| make_field_ident(s))
1252 .collect();
1253
1254 let within_pkg = if current_package.is_empty() {
1256 proto_fqn
1257 } else {
1258 proto_fqn
1259 .strip_prefix(current_package)
1260 .and_then(|s| s.strip_prefix('.'))
1261 .unwrap_or(proto_fqn)
1262 };
1263 let msg_segs: Vec<_> = within_pkg
1264 .split('.')
1265 .filter(|s| !s.is_empty())
1266 .map(|name| make_field_ident(&to_snake_case(name)))
1267 .collect();
1268
1269 quote! { #supers_tokens #sentinel :: #(#kind_segs ::)* #(#msg_segs ::)* }
1270}
1271
1272pub(crate) fn matches_proto_prefix(prefix: &str, fqn_dotted: &str) -> bool {
1277 prefix == "."
1278 || prefix == fqn_dotted
1279 || (fqn_dotted.starts_with(prefix)
1280 && fqn_dotted.as_bytes().get(prefix.len()) == Some(&b'.'))
1281}
1282
1283fn resolve_file_extern<'p>(
1292 file_name: &str,
1293 file_extern_paths: &'p [(String, String)],
1294) -> Option<&'p str> {
1295 file_extern_paths
1296 .iter()
1297 .find(|(name, _)| name == file_name)
1298 .map(|(_, rust)| rust.as_str())
1299}
1300
1301pub(crate) fn resolve_extern_prefix(
1312 package: &str,
1313 extern_paths: &[(String, String)],
1314) -> Option<String> {
1315 let dotted = format!(".{}", package);
1316
1317 let mut best: Option<(&str, &str, usize)> = None;
1320
1321 for (proto_prefix, rust_prefix) in extern_paths {
1322 if dotted == *proto_prefix {
1323 return Some(rust_prefix.clone());
1325 }
1326 if let Some(rest) = dotted.strip_prefix(proto_prefix.as_str()) {
1327 if proto_prefix == "." || rest.starts_with('.') {
1329 let prefix_len = proto_prefix.len();
1330 if best.map_or(true, |(_, _, best_len)| prefix_len > best_len) {
1332 best = Some((proto_prefix, rust_prefix, prefix_len));
1333 }
1334 }
1335 }
1336 }
1337
1338 let (proto_prefix, rust_prefix, _) = best?;
1339 let rest = dotted.strip_prefix(proto_prefix)?;
1340 let rest = rest.strip_prefix('.').unwrap_or(rest);
1341 let suffix = rest
1342 .split('.')
1343 .map(to_snake_case)
1344 .collect::<Vec<_>>()
1345 .join("::");
1346 Some(format!("{}::{}", rust_prefix, suffix))
1347}
1348
1349fn resolve_extern_type(fqn: &str, extern_paths: &[(String, String)]) -> Option<String> {
1368 if let Some((_, rust)) = extern_paths.iter().find(|(proto, _)| proto == fqn) {
1370 return Some(rust.clone());
1371 }
1372
1373 let mut best: Option<(&str, &str, usize)> = None;
1375 for (proto_prefix, rust_prefix) in extern_paths {
1376 let matches = proto_prefix == "."
1377 || fqn
1378 .strip_prefix(proto_prefix.as_str())
1379 .is_some_and(|rest| rest.starts_with('.'));
1380 if matches && best.map_or(true, |(_, _, best_len)| proto_prefix.len() > best_len) {
1382 best = Some((proto_prefix, rust_prefix, proto_prefix.len()));
1383 }
1384 }
1385
1386 let (proto_prefix, rust_prefix, _) = best?;
1387 let rest = if proto_prefix == "." {
1390 fqn.strip_prefix('.').unwrap_or(fqn)
1391 } else {
1392 fqn.strip_prefix(proto_prefix)
1393 .and_then(|r| r.strip_prefix('.'))
1394 .unwrap_or("")
1395 };
1396 let mut segments = rest.split('.').collect::<Vec<_>>();
1397 let type_name = segments.pop()?;
1399 let mut path = rust_prefix.to_string();
1400 for module in segments {
1401 path.push_str("::");
1402 path.push_str(&to_snake_case(module));
1403 }
1404 path.push_str("::");
1405 path.push_str(type_name);
1406 Some(path)
1407}
1408
1409fn join_mod(module: &str, name: &str) -> String {
1412 if module.is_empty() {
1413 name.to_string()
1414 } else {
1415 format!("{module}::{name}")
1416 }
1417}
1418
1419fn resolve_type_path(
1432 fqn: &str,
1433 name: &str,
1434 file_root: Option<&str>,
1435 local_module: &str,
1436 extern_paths: &[(String, String)],
1437 type_name_prefix: &str,
1438) -> (String, bool) {
1439 if let Some((_, rust)) = extern_paths.iter().find(|(proto, _)| proto == fqn) {
1449 (rust.clone(), true)
1450 } else if let Some(root) = file_root {
1451 (join_mod(root, name), true)
1452 } else if let Some(path) = resolve_extern_type(fqn, extern_paths) {
1453 (path, true)
1454 } else {
1455 (
1456 join_mod(local_module, &format!("{type_name_prefix}{name}")),
1457 false,
1458 )
1459 }
1460}
1461
1462#[derive(Clone, Copy)]
1465struct NestedRegistrationCtx<'a> {
1466 package: &'a str,
1467 extern_paths: &'a [(String, String)],
1468 type_name_prefix: &'a str,
1469}
1470
1471fn register_nested_types(
1481 type_map: &mut HashMap<String, String>,
1482 package_of: &mut HashMap<String, String>,
1483 reg: NestedRegistrationCtx<'_>,
1484 parent_fqn: &str,
1485 parent_mod: &str,
1486 msg: &crate::generated::descriptor::DescriptorProto,
1487) {
1488 let NestedRegistrationCtx {
1489 package,
1490 extern_paths,
1491 type_name_prefix,
1492 } = reg;
1493 for nested in &msg.nested_type {
1494 if let Some(name) = &nested.name {
1495 let fqn = format!("{}.{}", parent_fqn, name);
1496 let (rust_path, child_mod) = match extern_paths.iter().find(|(proto, _)| proto == &fqn)
1502 {
1503 Some((_, rust)) => {
1504 let child = match rust.rsplit_once("::") {
1505 Some((parent, _)) => format!("{parent}::{}", to_snake_case(name)),
1506 None => to_snake_case(name),
1507 };
1508 (rust.clone(), child)
1509 }
1510 None => (
1511 format!("{parent_mod}::{type_name_prefix}{name}"),
1512 format!("{parent_mod}::{}", to_snake_case(name)),
1513 ),
1514 };
1515 type_map.insert(fqn.clone(), rust_path);
1516 package_of.insert(fqn.clone(), package.to_string());
1517
1518 register_nested_types(type_map, package_of, reg, &fqn, &child_mod, nested);
1520 }
1521 }
1522
1523 for enum_type in &msg.enum_type {
1524 if let Some(name) = &enum_type.name {
1525 let fqn = format!("{}.{}", parent_fqn, name);
1526 let rust_path = extern_paths
1527 .iter()
1528 .find(|(proto, _)| proto == &fqn)
1529 .map(|(_, rust)| rust.clone())
1530 .unwrap_or_else(|| format!("{parent_mod}::{type_name_prefix}{name}"));
1531 type_map.insert(fqn.clone(), rust_path);
1532 package_of.insert(fqn, package.to_string());
1533 }
1534 }
1535}
1536
1537fn collect_enum_first_values(files: &[FileDescriptorProto]) -> HashMap<String, i32> {
1540 fn record(map: &mut HashMap<String, i32>, prefix: &str, e: &EnumDescriptorProto) {
1541 if let (Some(name), Some(first)) = (e.name.as_deref(), e.value.first()) {
1542 map.insert(format!("{prefix}{name}"), first.number.unwrap_or(0));
1543 }
1544 }
1545 fn walk_msg(map: &mut HashMap<String, i32>, prefix: &str, msg: &DescriptorProto) {
1546 let Some(name) = msg.name.as_deref() else {
1547 return;
1548 };
1549 let child_prefix = format!("{prefix}{name}.");
1550 for e in &msg.enum_type {
1551 record(map, &child_prefix, e);
1552 }
1553 for nested in &msg.nested_type {
1554 walk_msg(map, &child_prefix, nested);
1555 }
1556 }
1557
1558 let mut map = HashMap::new();
1559 for file in files {
1560 let prefix = match file.package.as_deref() {
1561 Some(p) if !p.is_empty() => format!(".{p}."),
1562 _ => ".".to_string(),
1563 };
1564 for e in &file.enum_type {
1565 record(&mut map, &prefix, e);
1566 }
1567 for msg in &file.message_type {
1568 walk_msg(&mut map, &prefix, msg);
1569 }
1570 }
1571 map
1572}
1573
1574fn register_enum_closedness(
1576 map: &mut HashMap<String, bool>,
1577 fqn: &str,
1578 parent_features: &ResolvedFeatures,
1579 enum_desc: &EnumDescriptorProto,
1580) {
1581 let resolved = features::resolve_child(parent_features, features::enum_features(enum_desc));
1582 let closed = resolved.enum_type == features::EnumType::Closed;
1583 map.insert(fqn.to_string(), closed);
1584}
1585
1586fn register_nested_enum_closedness(
1589 map: &mut HashMap<String, bool>,
1590 parent_fqn: &str,
1591 parent_features: &ResolvedFeatures,
1592 msg: &DescriptorProto,
1593) {
1594 let msg_features = features::resolve_child(parent_features, features::message_features(msg));
1595 for enum_type in &msg.enum_type {
1596 if let Some(name) = &enum_type.name {
1597 let fqn = format!("{}.{}", parent_fqn, name);
1598 register_enum_closedness(map, &fqn, &msg_features, enum_type);
1599 }
1600 }
1601 for nested in &msg.nested_type {
1602 if let Some(name) = &nested.name {
1603 let fqn = format!("{}.{}", parent_fqn, name);
1604 register_nested_enum_closedness(map, &fqn, &msg_features, nested);
1605 }
1606 }
1607}
1608
1609#[cfg(test)]
1610mod tests {
1611 use super::*;
1612 use crate::generated::descriptor::{DescriptorProto, EnumDescriptorProto, FileDescriptorProto};
1613
1614 fn children(segs: &[&str]) -> HashSet<String> {
1615 segs.iter().map(|s| s.to_string()).collect()
1616 }
1617
1618 fn names(ns: &[&str]) -> Vec<String> {
1619 ns.iter().map(|s| s.to_string()).collect()
1620 }
1621
1622 #[test]
1623 fn deconflict_no_collision_keeps_base() {
1624 let out = deconflict_package_modules(&names(&["Oof", "Bar"]), &children(&["other"]));
1626 assert_eq!(out, vec!["oof".to_string(), "bar".to_string()]);
1627 }
1628
1629 #[test]
1630 fn deconflict_single_collision_appends_underscore() {
1631 let out = deconflict_package_modules(&names(&["Oof"]), &children(&["oof"]));
1632 assert_eq!(out, vec!["oof_".to_string()]);
1633 }
1634
1635 #[test]
1636 fn deconflict_repeated_append_when_underscore_slot_also_taken() {
1637 let out = deconflict_package_modules(&names(&["Oof"]), &children(&["oof", "oof_"]));
1639 assert_eq!(out, vec!["oof__".to_string()]);
1640 }
1641
1642 #[test]
1643 fn deconflict_two_messages_racing_to_same_slot_stay_distinct() {
1644 let out = deconflict_package_modules(&names(&["Oof", "Oof_"]), &children(&["oof", "oof_"]));
1647 assert_eq!(out, vec!["oof__".to_string(), "oof___".to_string()]);
1648 let set: HashSet<&String> = out.iter().collect();
1650 assert_eq!(set.len(), out.len());
1651 assert!(!out.contains(&"oof".to_string()) && !out.contains(&"oof_".to_string()));
1652 }
1653
1654 #[test]
1655 fn deconflict_is_independent_of_declaration_order() {
1656 let ch = children(&["oof", "oof_"]);
1658 let fwd = deconflict_package_modules(&names(&["Oof", "Oof_"]), &ch);
1659 let rev = deconflict_package_modules(&names(&["Oof_", "Oof"]), &ch);
1660 assert_eq!(fwd, vec!["oof__".to_string(), "oof___".to_string()]);
1662 assert_eq!(rev, vec!["oof___".to_string(), "oof__".to_string()]);
1663 }
1664
1665 #[test]
1666 fn deconflict_avoids_other_messages_raw_base() {
1667 let out = deconflict_package_modules(&names(&["Oof", "Oof_"]), &children(&["oof"]));
1670 assert_eq!(out, vec!["oof__".to_string(), "oof_".to_string()]);
1672 }
1673
1674 #[test]
1675 fn deconflict_never_yields_the_sentinel() {
1676 let out = deconflict_package_modules(&names(&["Buffa"]), &children(&["__buffa", "buffa"]));
1677 assert_eq!(out, vec!["buffa_".to_string()]);
1679 assert_ne!(out[0], SENTINEL_MOD);
1680 }
1681
1682 #[test]
1683 fn child_package_segments_extracts_immediate_segment() {
1684 let pkgs = children(&["foo", "foo.oof", "foo.bar.baz", "foobar"]);
1685 let mut got: Vec<String> = child_package_segments("foo", &pkgs).into_iter().collect();
1686 got.sort();
1687 assert_eq!(got, vec!["bar".to_string(), "oof".to_string()]);
1689 }
1690
1691 fn make_file(
1692 name: &str,
1693 package: &str,
1694 messages: Vec<DescriptorProto>,
1695 enums: Vec<EnumDescriptorProto>,
1696 ) -> FileDescriptorProto {
1697 FileDescriptorProto {
1698 name: Some(name.to_string()),
1699 package: if package.is_empty() {
1700 None
1701 } else {
1702 Some(package.to_string())
1703 },
1704 message_type: messages,
1705 enum_type: enums,
1706 ..Default::default()
1707 }
1708 }
1709
1710 fn msg(name: &str) -> DescriptorProto {
1711 DescriptorProto {
1712 name: Some(name.to_string()),
1713 ..Default::default()
1714 }
1715 }
1716
1717 fn msg_with_nested(name: &str, nested: Vec<DescriptorProto>) -> DescriptorProto {
1718 DescriptorProto {
1719 name: Some(name.to_string()),
1720 nested_type: nested,
1721 ..Default::default()
1722 }
1723 }
1724
1725 fn msg_with_nested_and_enums(
1726 name: &str,
1727 nested: Vec<DescriptorProto>,
1728 enums: Vec<EnumDescriptorProto>,
1729 ) -> DescriptorProto {
1730 DescriptorProto {
1731 name: Some(name.to_string()),
1732 nested_type: nested,
1733 enum_type: enums,
1734 ..Default::default()
1735 }
1736 }
1737
1738 fn enum_desc(name: &str) -> EnumDescriptorProto {
1739 EnumDescriptorProto {
1740 name: Some(name.to_string()),
1741 ..Default::default()
1742 }
1743 }
1744
1745 fn enum_with_closed_feature(name: &str) -> EnumDescriptorProto {
1746 use crate::generated::descriptor::{feature_set, EnumOptions, FeatureSet};
1747 EnumDescriptorProto {
1748 name: Some(name.to_string()),
1749 options: buffa::MessageField::some(EnumOptions {
1750 features: buffa::MessageField::some(FeatureSet {
1751 enum_type: Some(feature_set::EnumType::CLOSED),
1752 ..Default::default()
1753 }),
1754 ..Default::default()
1755 }),
1756 ..Default::default()
1757 }
1758 }
1759
1760 fn editions_file(
1761 name: &str,
1762 package: &str,
1763 messages: Vec<DescriptorProto>,
1764 enums: Vec<EnumDescriptorProto>,
1765 ) -> FileDescriptorProto {
1766 use crate::generated::descriptor::Edition;
1767 FileDescriptorProto {
1768 name: Some(name.to_string()),
1769 package: Some(package.to_string()),
1770 syntax: Some("editions".to_string()),
1771 edition: Some(Edition::EDITION_2023),
1772 message_type: messages,
1773 enum_type: enums,
1774 ..Default::default()
1775 }
1776 }
1777
1778 #[test]
1781 fn test_message_with_package() {
1782 let files = [make_file(
1783 "test.proto",
1784 "my.package",
1785 vec![msg("Foo")],
1786 vec![],
1787 )];
1788 let config = CodeGenConfig::default();
1789 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1790 assert_eq!(ctx.rust_type(".my.package.Foo"), Some("my::package::Foo"));
1791 }
1792
1793 #[test]
1794 fn test_message_no_package() {
1795 let files = [make_file("test.proto", "", vec![msg("Bar")], vec![])];
1796 let config = CodeGenConfig::default();
1797 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1798 assert_eq!(ctx.rust_type(".Bar"), Some("Bar"));
1799 }
1800
1801 #[test]
1802 fn test_nested_message_uses_module_path() {
1803 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
1804 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
1805 let config = CodeGenConfig::default();
1806 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1807 assert_eq!(ctx.rust_type(".pkg.Outer"), Some("pkg::Outer"));
1808 assert_eq!(ctx.rust_type(".pkg.Outer.Inner"), Some("pkg::outer::Inner"));
1810 }
1811
1812 #[test]
1813 fn test_nested_message_no_package() {
1814 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
1815 let files = [make_file("test.proto", "", vec![outer], vec![])];
1816 let config = CodeGenConfig::default();
1817 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1818 assert_eq!(ctx.rust_type(".Outer"), Some("Outer"));
1819 assert_eq!(ctx.rust_type(".Outer.Inner"), Some("outer::Inner"));
1820 }
1821
1822 #[test]
1823 fn test_deeply_nested_message() {
1824 let deep = msg_with_nested("A", vec![msg_with_nested("B", vec![msg("C")])]);
1825 let files = [make_file("test.proto", "pkg", vec![deep], vec![])];
1826 let config = CodeGenConfig::default();
1827 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1828 assert_eq!(ctx.rust_type(".pkg.A"), Some("pkg::A"));
1829 assert_eq!(ctx.rust_type(".pkg.A.B"), Some("pkg::a::B"));
1830 assert_eq!(ctx.rust_type(".pkg.A.B.C"), Some("pkg::a::b::C"));
1831 }
1832
1833 #[test]
1834 fn test_nested_enum_uses_module_path() {
1835 let outer = msg_with_nested_and_enums("Outer", vec![], vec![enum_desc("Status")]);
1836 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
1837 let config = CodeGenConfig::default();
1838 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1839 assert_eq!(
1840 ctx.rust_type(".pkg.Outer.Status"),
1841 Some("pkg::outer::Status")
1842 );
1843 }
1844
1845 #[test]
1846 fn test_top_level_enum() {
1847 let files = [make_file(
1848 "test.proto",
1849 "pkg",
1850 vec![],
1851 vec![enum_desc("Status")],
1852 )];
1853 let config = CodeGenConfig::default();
1854 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1855 assert_eq!(ctx.rust_type(".pkg.Status"), Some("pkg::Status"));
1856 }
1857
1858 #[test]
1859 fn test_same_named_nested_types_in_different_parents_are_distinct() {
1860 let outer1 = msg_with_nested("Outer1", vec![msg("Inner")]);
1861 let outer2 = msg_with_nested("Outer2", vec![msg("Inner")]);
1862 let files = [make_file("a.proto", "pkg", vec![outer1, outer2], vec![])];
1863 let config = CodeGenConfig::default();
1864 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1865 assert_eq!(
1867 ctx.rust_type(".pkg.Outer1.Inner"),
1868 Some("pkg::outer1::Inner")
1869 );
1870 assert_eq!(
1871 ctx.rust_type(".pkg.Outer2.Inner"),
1872 Some("pkg::outer2::Inner")
1873 );
1874 assert_ne!(
1875 ctx.rust_type(".pkg.Outer1.Inner"),
1876 ctx.rust_type(".pkg.Outer2.Inner")
1877 );
1878 }
1879
1880 #[test]
1881 fn test_multiple_files() {
1882 let files = [
1883 make_file("a.proto", "ns.a", vec![msg("MsgA")], vec![]),
1884 make_file("b.proto", "ns.b", vec![msg("MsgB")], vec![]),
1885 ];
1886 let config = CodeGenConfig::default();
1887 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1888 assert_eq!(ctx.rust_type(".ns.a.MsgA"), Some("ns::a::MsgA"));
1889 assert_eq!(ctx.rust_type(".ns.b.MsgB"), Some("ns::b::MsgB"));
1890 }
1891
1892 #[test]
1902 fn test_extern_path_exact_per_type_match() {
1903 let files = [make_file(
1904 "test.proto",
1905 "test.pkg",
1906 vec![msg("Msg")],
1907 vec![],
1908 )];
1909 let config = CodeGenConfig {
1910 extern_paths: vec![(".test.pkg.Msg".into(), "::ext_crate::Msg".into())],
1911 ..Default::default()
1912 };
1913 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1914 assert_eq!(ctx.rust_type(".test.pkg.Msg"), Some("::ext_crate::Msg"));
1916 }
1917
1918 #[test]
1919 fn test_extern_path_per_type_overrides_package_prefix() {
1920 let files = [make_file(
1924 "test.proto",
1925 "test.pkg",
1926 vec![msg("Msg"), msg("Other")],
1927 vec![],
1928 )];
1929 let config = CodeGenConfig {
1930 extern_paths: vec![
1931 (".test.pkg".into(), "::pkg_crate".into()),
1932 (".test.pkg.Msg".into(), "::ext_crate::Msg".into()),
1933 ],
1934 ..Default::default()
1935 };
1936 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1937 assert_eq!(ctx.rust_type(".test.pkg.Msg"), Some("::ext_crate::Msg"));
1938 assert_eq!(ctx.rust_type(".test.pkg.Other"), Some("::pkg_crate::Other"));
1939 }
1940
1941 #[test]
1942 fn test_extern_path_nested_type_inherits_per_type_override() {
1943 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
1946 let files = [make_file("test.proto", "test.pkg", vec![outer], vec![])];
1947 let config = CodeGenConfig {
1948 extern_paths: vec![(".test.pkg.Outer".into(), "::ext::Outer".into())],
1949 ..Default::default()
1950 };
1951 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1952 assert_eq!(ctx.rust_type(".test.pkg.Outer"), Some("::ext::Outer"));
1953 assert_eq!(
1954 ctx.rust_type(".test.pkg.Outer.Inner"),
1955 Some("::ext::outer::Inner")
1956 );
1957 }
1958
1959 #[test]
1960 fn test_extern_path_exact_per_type_enum() {
1961 let files = [make_file(
1962 "test.proto",
1963 "test.pkg",
1964 vec![],
1965 vec![enum_desc("Status")],
1966 )];
1967 let config = CodeGenConfig {
1968 extern_paths: vec![(".test.pkg.Status".into(), "::ext_crate::Status".into())],
1969 ..Default::default()
1970 };
1971 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1972 assert_eq!(
1973 ctx.rust_type(".test.pkg.Status"),
1974 Some("::ext_crate::Status")
1975 );
1976 }
1977
1978 #[test]
1979 fn test_extern_path_package_prefix_still_resolves() {
1980 let files = [make_file(
1983 "test.proto",
1984 "test.pkg",
1985 vec![msg("Msg")],
1986 vec![],
1987 )];
1988 let config = CodeGenConfig {
1989 extern_paths: vec![(".test.pkg".into(), "::pkg_crate".into())],
1990 ..Default::default()
1991 };
1992 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
1993 assert_eq!(ctx.rust_type(".test.pkg.Msg"), Some("::pkg_crate::Msg"));
1994 }
1995
1996 #[test]
1997 fn test_extern_path_per_type_does_not_affect_unmapped_type() {
1998 let files = [make_file(
2000 "test.proto",
2001 "test.pkg",
2002 vec![msg("Msg"), msg("Other")],
2003 vec![],
2004 )];
2005 let config = CodeGenConfig {
2006 extern_paths: vec![(".test.pkg.Msg".into(), "::ext_crate::Msg".into())],
2007 ..Default::default()
2008 };
2009 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2010 assert_eq!(ctx.rust_type(".test.pkg.Msg"), Some("::ext_crate::Msg"));
2011 assert_eq!(ctx.rust_type(".test.pkg.Other"), Some("test::pkg::Other"));
2013 }
2014
2015 #[test]
2016 fn test_keyword_package_segment_in_type_map() {
2017 let files = [make_file(
2020 "latlng.proto",
2021 "google.type",
2022 vec![msg("LatLng")],
2023 vec![],
2024 )];
2025 let config = CodeGenConfig::default();
2026 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2027 assert_eq!(
2028 ctx.rust_type(".google.type.LatLng"),
2029 Some("google::type::LatLng")
2030 );
2031 }
2032
2033 #[test]
2034 fn test_keyword_package_relative_same_package() {
2035 let files = [make_file(
2036 "latlng.proto",
2037 "google.type",
2038 vec![msg("LatLng"), msg("Expr")],
2039 vec![],
2040 )];
2041 let config = CodeGenConfig::default();
2042 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2043 assert_eq!(
2045 ctx.rust_type_relative(".google.type.LatLng", "google.type", 0),
2046 Some("LatLng".into())
2047 );
2048 }
2049
2050 #[test]
2051 fn test_keyword_package_cross_package() {
2052 let files = [
2053 make_file("latlng.proto", "google.type", vec![msg("LatLng")], vec![]),
2054 make_file("svc.proto", "google.cloud", vec![msg("Service")], vec![]),
2055 ];
2056 let config = CodeGenConfig::default();
2057 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2058 assert_eq!(
2061 ctx.rust_type_relative(".google.type.LatLng", "google.cloud", 0),
2062 Some("super::type::LatLng".into())
2063 );
2064 }
2065
2066 #[test]
2067 fn test_keyword_nested_message_module() {
2068 let outer = msg_with_nested("Type", vec![msg("Inner")]);
2070 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
2071 let config = CodeGenConfig::default();
2072 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2073 assert_eq!(ctx.rust_type(".pkg.Type"), Some("pkg::Type"));
2074 assert_eq!(ctx.rust_type(".pkg.Type.Inner"), Some("pkg::type::Inner"));
2075 }
2076
2077 #[test]
2078 fn test_unknown_type_returns_none() {
2079 let files = [make_file("test.proto", "pkg", vec![msg("Foo")], vec![])];
2080 let config = CodeGenConfig::default();
2081 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2082 assert_eq!(ctx.rust_type(".pkg.Unknown"), None);
2083 }
2084
2085 #[test]
2088 fn test_relative_same_package_top_level() {
2089 let files = [make_file("a.proto", "pkg", vec![msg("Foo")], vec![])];
2090 let config = CodeGenConfig::default();
2091 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2092 assert_eq!(
2094 ctx.rust_type_relative(".pkg.Foo", "pkg", 0),
2095 Some("Foo".into())
2096 );
2097 }
2098
2099 #[test]
2100 fn test_relative_cross_package() {
2101 let files = [
2102 make_file("a.proto", "pkg_a", vec![msg("Foo")], vec![]),
2103 make_file("b.proto", "pkg_b", vec![msg("Bar")], vec![]),
2104 ];
2105 let config = CodeGenConfig::default();
2106 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2107 assert_eq!(
2109 ctx.rust_type_relative(".pkg_a.Foo", "pkg_b", 0),
2110 Some("super::pkg_a::Foo".into())
2111 );
2112 }
2113
2114 #[test]
2115 fn test_relative_no_package() {
2116 let files = [make_file("a.proto", "", vec![msg("Foo")], vec![])];
2117 let config = CodeGenConfig::default();
2118 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2119 assert_eq!(ctx.rust_type_relative(".Foo", "", 0), Some("Foo".into()));
2120 }
2121
2122 #[test]
2123 fn test_relative_unknown_returns_none() {
2124 let files = [make_file("a.proto", "pkg", vec![msg("Foo")], vec![])];
2125 let config = CodeGenConfig::default();
2126 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2127 assert_eq!(ctx.rust_type_relative(".pkg.Unknown", "pkg", 0), None);
2128 }
2129
2130 #[test]
2131 fn test_relative_dotted_package() {
2132 let files = [make_file("a.proto", "my.pkg", vec![msg("Foo")], vec![])];
2133 let config = CodeGenConfig::default();
2134 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2135 assert_eq!(
2136 ctx.rust_type_relative(".my.pkg.Foo", "my.pkg", 0),
2137 Some("Foo".into())
2138 );
2139 }
2140
2141 #[test]
2142 fn test_relative_cross_dotted_packages() {
2143 let files = [
2144 make_file(
2145 "timestamp.proto",
2146 "google.protobuf",
2147 vec![msg("Timestamp")],
2148 vec![],
2149 ),
2150 make_file(
2151 "test.proto",
2152 "protobuf_test_messages.proto3",
2153 vec![msg("TestAllTypesProto3")],
2154 vec![],
2155 ),
2156 ];
2157 let config = CodeGenConfig::default();
2158 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2159
2160 assert_eq!(
2162 ctx.rust_type_relative(
2163 ".google.protobuf.Timestamp",
2164 "protobuf_test_messages.proto3",
2165 0,
2166 ),
2167 Some("super::super::google::protobuf::Timestamp".into())
2168 );
2169 }
2170
2171 #[test]
2172 fn test_relative_nested_type_from_same_package() {
2173 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
2175 let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
2176 let config = CodeGenConfig::default();
2177 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2178
2179 assert_eq!(
2181 ctx.rust_type_relative(".pkg.Outer.Inner", "pkg", 0),
2182 Some("outer::Inner".into())
2183 );
2184 }
2185
2186 #[test]
2187 fn test_relative_shared_prefix_not_confused() {
2188 let files = [
2189 make_file("ab.proto", "a.b", vec![msg("Msg1")], vec![]),
2190 make_file("abc.proto", "a.bc", vec![msg("Msg2")], vec![]),
2191 ];
2192 let config = CodeGenConfig::default();
2193 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2194
2195 assert_eq!(
2197 ctx.rust_type_relative(".a.b.Msg1", "a.bc", 0),
2198 Some("super::b::Msg1".into())
2199 );
2200 assert_eq!(
2202 ctx.rust_type_relative(".a.bc.Msg2", "a.b", 0),
2203 Some("super::bc::Msg2".into())
2204 );
2205 }
2206
2207 #[test]
2210 fn test_relative_cross_package_nesting_1() {
2211 let outer = msg_with_nested_and_enums("Business", vec![], vec![enum_desc("Status")]);
2215 let files = [
2216 make_file("admin.proto", "a.b.admin.v1", vec![msg("Svc")], vec![]),
2217 make_file("biz.proto", "a.b.v1", vec![outer], vec![]),
2218 ];
2219 let config = CodeGenConfig::default();
2220 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2221
2222 assert_eq!(
2224 ctx.rust_type_relative(".a.b.v1.Business.Status", "a.b.admin.v1", 0),
2225 Some("super::super::v1::business::Status".into())
2226 );
2227 assert_eq!(
2229 ctx.rust_type_relative(".a.b.v1.Business.Status", "a.b.admin.v1", 1),
2230 Some("super::super::super::v1::business::Status".into())
2231 );
2232 }
2233
2234 #[test]
2235 fn test_relative_same_package_nesting_1() {
2236 let files = [make_file(
2238 "test.proto",
2239 "pkg",
2240 vec![msg("Foo"), msg("Bar")],
2241 vec![],
2242 )];
2243 let config = CodeGenConfig::default();
2244 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2245
2246 assert_eq!(
2248 ctx.rust_type_relative(".pkg.Foo", "pkg", 0),
2249 Some("Foo".into())
2250 );
2251 assert_eq!(
2253 ctx.rust_type_relative(".pkg.Foo", "pkg", 1),
2254 Some("super::Foo".into())
2255 );
2256 assert_eq!(
2258 ctx.rust_type_relative(".pkg.Foo", "pkg", 2),
2259 Some("super::super::Foo".into())
2260 );
2261 }
2262
2263 #[test]
2266 fn test_resolve_file_extern_exact_match_only() {
2267 let mappings = [(
2268 "google/protobuf/descriptor.proto".to_string(),
2269 "::buffa_descriptor::generated::descriptor".to_string(),
2270 )];
2271 assert_eq!(
2273 resolve_file_extern("google/protobuf/descriptor.proto", &mappings),
2274 Some("::buffa_descriptor::generated::descriptor"),
2275 );
2276 assert_eq!(
2279 resolve_file_extern("google/protobuf/timestamp.proto", &mappings),
2280 None,
2281 );
2282 assert_eq!(
2284 resolve_file_extern("vendor/google/protobuf/descriptor.proto", &mappings),
2285 None,
2286 );
2287 }
2288
2289 #[test]
2290 fn test_resolve_extern_prefix_exact_match() {
2291 let result = resolve_extern_prefix(
2292 "my.common",
2293 &[(".my.common".into(), "::common_protos".into())],
2294 );
2295 assert_eq!(result, Some("::common_protos".into()));
2296 }
2297
2298 #[test]
2299 fn test_resolve_extern_prefix_sub_package() {
2300 let result = resolve_extern_prefix(
2301 "my.common.sub",
2302 &[(".my.common".into(), "::common_protos".into())],
2303 );
2304 assert_eq!(result, Some("::common_protos::sub".into()));
2305 }
2306
2307 #[test]
2308 fn test_resolve_extern_prefix_no_match() {
2309 let result = resolve_extern_prefix(
2310 "other.pkg",
2311 &[(".my.common".into(), "::common_protos".into())],
2312 );
2313 assert_eq!(result, None);
2314 }
2315
2316 #[test]
2317 fn test_resolve_extern_prefix_partial_name_no_match() {
2318 let result = resolve_extern_prefix(
2320 "my.commonext",
2321 &[(".my.common".into(), "::common_protos".into())],
2322 );
2323 assert_eq!(result, None);
2324 }
2325
2326 #[test]
2327 fn test_resolve_extern_prefix_longest_match_wins() {
2328 let result = resolve_extern_prefix(
2330 "my.common.sub",
2331 &[
2332 (".my".into(), "::crate_a".into()),
2333 (".my.common".into(), "::crate_b".into()),
2334 ],
2335 );
2336 assert_eq!(result, Some("::crate_b::sub".into()));
2337 }
2338
2339 #[test]
2340 fn test_resolve_extern_prefix_catchall() {
2341 let result = resolve_extern_prefix("greet.v1", &[(".".into(), "crate::proto".into())]);
2342 assert_eq!(result, Some("crate::proto::greet::v1".into()));
2343 }
2344
2345 #[test]
2346 fn test_resolve_extern_prefix_catchall_empty_pkg() {
2347 let result = resolve_extern_prefix("", &[(".".into(), "crate::proto".into())]);
2350 assert_eq!(result, Some("crate::proto".into()));
2351 }
2352
2353 #[test]
2354 fn test_resolve_extern_prefix_catchall_longest_wins() {
2355 let result = resolve_extern_prefix(
2358 "google.protobuf",
2359 &[
2360 (".".into(), "crate::proto".into()),
2361 (
2362 ".google.protobuf".into(),
2363 "::buffa_types::google::protobuf".into(),
2364 ),
2365 ],
2366 );
2367 assert_eq!(result, Some("::buffa_types::google::protobuf".into()));
2368 }
2369
2370 #[test]
2371 fn test_resolve_extern_prefix_catchall_keyword_package() {
2372 let result = resolve_extern_prefix("google.type", &[(".".into(), "crate::proto".into())]);
2375 assert_eq!(result, Some("crate::proto::google::type".into()));
2376 }
2377
2378 #[test]
2381 fn test_resolve_extern_type_exact_match() {
2382 let result = resolve_extern_type(
2384 ".google.protobuf.Timestamp",
2385 &[(
2386 ".google.protobuf.Timestamp".into(),
2387 "::pbjson_types::Timestamp".into(),
2388 )],
2389 );
2390 assert_eq!(result, Some("::pbjson_types::Timestamp".into()));
2391 }
2392
2393 #[test]
2394 fn test_resolve_extern_type_exact_wins_over_prefix() {
2395 let result = resolve_extern_type(
2397 ".my.pkg.Msg",
2398 &[
2399 (".my.pkg".into(), "::pkg_crate".into()),
2400 (".my.pkg.Msg".into(), "::ext_crate::Msg".into()),
2401 ],
2402 );
2403 assert_eq!(result, Some("::ext_crate::Msg".into()));
2404 }
2405
2406 #[test]
2407 fn test_resolve_extern_type_package_prefix_appends_type() {
2408 let result = resolve_extern_type(
2411 ".my.common.sub.Msg",
2412 &[(".my.common".into(), "::common_protos".into())],
2413 );
2414 assert_eq!(result, Some("::common_protos::sub::Msg".into()));
2415 }
2416
2417 #[test]
2418 fn test_resolve_extern_type_catchall() {
2419 let result = resolve_extern_type(".greet.v1.Hello", &[(".".into(), "crate::proto".into())]);
2420 assert_eq!(result, Some("crate::proto::greet::v1::Hello".into()));
2421 }
2422
2423 #[test]
2424 fn test_resolve_extern_type_no_match() {
2425 let result = resolve_extern_type(
2426 ".other.pkg.Msg",
2427 &[(".my.common".into(), "::common_protos".into())],
2428 );
2429 assert_eq!(result, None);
2430 }
2431
2432 #[test]
2433 fn test_resolve_extern_type_partial_name_no_match() {
2434 let result = resolve_extern_type(
2436 ".my.commonext.Msg",
2437 &[(".my.common".into(), "::common_protos".into())],
2438 );
2439 assert_eq!(result, None);
2440 }
2441
2442 #[test]
2445 fn test_split_extern_top_level() {
2446 let outer = msg_with_nested("Value", vec![msg("Inner")]);
2447 let files = [make_file(
2448 "struct.proto",
2449 "google.protobuf",
2450 vec![outer],
2451 vec![],
2452 )];
2453 let config = CodeGenConfig::default();
2454 let extern_paths = vec![(
2455 ".google.protobuf".into(),
2456 "::buffa_types::google::protobuf".into(),
2457 )];
2458 let ctx = CodeGenContext::new(&files, &config, &extern_paths);
2459
2460 let split = ctx
2461 .rust_type_relative_split(".google.protobuf.Value", "my.pkg", 3)
2462 .expect("type resolves");
2463 assert!(split.is_extern);
2464 assert_eq!(split.to_package, "::buffa_types::google::protobuf");
2466 assert_eq!(split.within_package, "Value");
2467 }
2468
2469 #[test]
2470 fn test_split_extern_nested_type() {
2471 let outer = msg_with_nested("Value", vec![msg("Inner")]);
2476 let files = [make_file(
2477 "struct.proto",
2478 "google.protobuf",
2479 vec![outer],
2480 vec![],
2481 )];
2482 let config = CodeGenConfig::default();
2483 let extern_paths = vec![(
2484 ".google.protobuf".into(),
2485 "::buffa_types::google::protobuf".into(),
2486 )];
2487 let ctx = CodeGenContext::new(&files, &config, &extern_paths);
2488
2489 let split = ctx
2490 .rust_type_relative_split(".google.protobuf.Value.Inner", "my.pkg", 0)
2491 .expect("nested type resolves");
2492 assert!(split.is_extern);
2493 assert_eq!(split.to_package, "::buffa_types::google::protobuf");
2494 assert_eq!(split.within_package, "value::Inner");
2495 }
2496
2497 #[test]
2498 fn test_split_per_type_extern_override() {
2499 let outer = msg_with_nested("Outer", vec![msg("Inner")]);
2505 let files = [make_file("custom.proto", "my.pkg", vec![outer], vec![])];
2506 let config = CodeGenConfig {
2507 extern_paths: vec![(".my.pkg.Outer".into(), "::ext::custom::Outer".into())],
2508 ..Default::default()
2509 };
2510 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2511
2512 let split = ctx
2513 .rust_type_relative_split(".my.pkg.Outer", "other.pkg", 2)
2514 .expect("overridden type resolves");
2515 assert!(split.is_extern);
2516 assert_eq!(split.to_package, "::ext::custom");
2517 assert_eq!(split.within_package, "Outer");
2518
2519 let nested = ctx
2522 .rust_type_relative_split(".my.pkg.Outer.Inner", "other.pkg", 0)
2523 .expect("nested type resolves");
2524 assert!(nested.is_extern);
2525 assert_eq!(nested.to_package, "::ext::custom");
2526 assert_eq!(nested.within_package, "outer::Inner");
2527 }
2528
2529 #[test]
2530 fn test_extern_path_top_level_message() {
2531 let files = [make_file(
2532 "common.proto",
2533 "my.common",
2534 vec![msg("SharedMsg")],
2535 vec![],
2536 )];
2537 let config = CodeGenConfig {
2538 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
2539 ..Default::default()
2540 };
2541 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2542 assert_eq!(
2543 ctx.rust_type(".my.common.SharedMsg"),
2544 Some("::common_protos::SharedMsg")
2545 );
2546 }
2547
2548 #[test]
2549 fn test_extern_path_nested_message() {
2550 let files = [make_file(
2551 "common.proto",
2552 "my.common",
2553 vec![msg_with_nested("Outer", vec![msg("Inner")])],
2554 vec![],
2555 )];
2556 let config = CodeGenConfig {
2557 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
2558 ..Default::default()
2559 };
2560 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2561 assert_eq!(
2562 ctx.rust_type(".my.common.Outer"),
2563 Some("::common_protos::Outer")
2564 );
2565 assert_eq!(
2566 ctx.rust_type(".my.common.Outer.Inner"),
2567 Some("::common_protos::outer::Inner")
2568 );
2569 }
2570
2571 #[test]
2572 fn test_extern_path_nested_types_use_deconflicted_module() {
2573 let money =
2575 msg_with_nested_and_enums("Money", vec![msg("Breakdown")], vec![enum_desc("Currency")]);
2576 let files = [
2577 make_file("lyft_money.proto", "pb.lyft", vec![money], vec![]),
2578 make_file("money.proto", "pb.lyft.money", vec![msg("Money")], vec![]),
2579 make_file(
2580 "users.proto",
2581 "pb.lyft.users",
2582 vec![msg("DailyTotalFares")],
2583 vec![],
2584 ),
2585 ];
2586 let config = CodeGenConfig {
2587 extern_paths: vec![(".pb.lyft".into(), "::idl_pb_lyft::pb::lyft".into())],
2588 ..Default::default()
2589 };
2590 let ctx = CodeGenContext::for_generate(&files, &["users.proto".to_string()], &config);
2591 assert_eq!(
2592 ctx.rust_type(".pb.lyft.Money.Currency"),
2593 Some("::idl_pb_lyft::pb::lyft::money_::Currency")
2594 );
2595 assert_eq!(
2596 ctx.rust_type(".pb.lyft.Money.Breakdown"),
2597 Some("::idl_pb_lyft::pb::lyft::money_::Breakdown")
2598 );
2599 }
2600
2601 #[test]
2602 fn test_extern_path_enum() {
2603 let files = [make_file(
2604 "common.proto",
2605 "my.common",
2606 vec![],
2607 vec![enum_desc("Status")],
2608 )];
2609 let config = CodeGenConfig {
2610 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
2611 ..Default::default()
2612 };
2613 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2614 assert_eq!(
2615 ctx.rust_type(".my.common.Status"),
2616 Some("::common_protos::Status")
2617 );
2618 }
2619
2620 #[test]
2621 fn test_extern_path_does_not_affect_other_packages() {
2622 let files = [
2623 make_file("common.proto", "my.common", vec![msg("SharedMsg")], vec![]),
2624 make_file(
2625 "service.proto",
2626 "my.service",
2627 vec![msg("MyService")],
2628 vec![],
2629 ),
2630 ];
2631 let config = CodeGenConfig {
2632 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
2633 ..Default::default()
2634 };
2635 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2636 assert_eq!(
2638 ctx.rust_type(".my.common.SharedMsg"),
2639 Some("::common_protos::SharedMsg")
2640 );
2641 assert_eq!(
2643 ctx.rust_type(".my.service.MyService"),
2644 Some("my::service::MyService")
2645 );
2646 }
2647
2648 #[test]
2649 fn test_extern_path_relative_returns_absolute() {
2650 let files = [
2653 make_file("common.proto", "my.common", vec![msg("SharedMsg")], vec![]),
2654 make_file(
2655 "service.proto",
2656 "my.service",
2657 vec![msg("MyService")],
2658 vec![],
2659 ),
2660 ];
2661 let config = CodeGenConfig {
2662 extern_paths: vec![(".my.common".into(), "::common_protos".into())],
2663 ..Default::default()
2664 };
2665 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2666 assert_eq!(
2668 ctx.rust_type_relative(".my.common.SharedMsg", "my.service", 0),
2669 Some("::common_protos::SharedMsg".into())
2670 );
2671 }
2672
2673 #[test]
2676 fn test_is_enum_closed_proto3_default_open() {
2677 let files = [make_file("a.proto", "p", vec![], vec![enum_desc("E")])];
2678 let config = CodeGenConfig::default();
2679 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2680 assert_eq!(ctx.is_enum_closed(".p.E"), Some(true));
2684 }
2685
2686 #[test]
2687 fn test_is_enum_closed_editions_default_open() {
2688 let files = [editions_file("a.proto", "p", vec![], vec![enum_desc("E")])];
2689 let config = CodeGenConfig::default();
2690 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2691 assert_eq!(ctx.is_enum_closed(".p.E"), Some(false));
2693 }
2694
2695 #[test]
2696 fn test_is_enum_closed_per_enum_override() {
2697 let files = [editions_file(
2700 "a.proto",
2701 "p",
2702 vec![],
2703 vec![enum_desc("Open"), enum_with_closed_feature("Closed")],
2704 )];
2705 let config = CodeGenConfig::default();
2706 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2707 assert_eq!(ctx.is_enum_closed(".p.Open"), Some(false));
2708 assert_eq!(ctx.is_enum_closed(".p.Closed"), Some(true));
2709 }
2710
2711 #[test]
2712 fn test_is_enum_closed_nested_per_enum_override() {
2713 let files = [editions_file(
2715 "a.proto",
2716 "p",
2717 vec![msg_with_nested_and_enums(
2718 "M",
2719 vec![],
2720 vec![enum_with_closed_feature("Inner")],
2721 )],
2722 vec![],
2723 )];
2724 let config = CodeGenConfig::default();
2725 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2726 assert_eq!(ctx.is_enum_closed(".p.M.Inner"), Some(true));
2727 }
2728
2729 #[test]
2730 fn test_is_enum_closed_unknown_enum_returns_none() {
2731 let files = [editions_file("a.proto", "p", vec![], vec![])];
2732 let config = CodeGenConfig::default();
2733 let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
2734 assert_eq!(ctx.is_enum_closed(".other.Unknown"), None);
2736 }
2737
2738 #[test]
2739 fn test_for_generate_auto_injects_wkt_mapping() {
2740 let ts_msg = DescriptorProto {
2743 name: Some("Timestamp".into()),
2744 ..Default::default()
2745 };
2746 let files = [FileDescriptorProto {
2747 name: Some("google/protobuf/timestamp.proto".into()),
2748 package: Some("google.protobuf".into()),
2749 syntax: Some("proto3".into()),
2750 message_type: vec![ts_msg],
2751 ..Default::default()
2752 }];
2753 let config = CodeGenConfig::default();
2754 let ctx = CodeGenContext::for_generate(&files, &["other.proto".into()], &config);
2756 assert_eq!(
2757 ctx.rust_type(".google.protobuf.Timestamp"),
2758 Some("::buffa_types::google::protobuf::Timestamp"),
2759 "WKT auto-mapping must be applied via for_generate"
2760 );
2761 }
2762
2763 #[test]
2764 fn test_for_generate_suppresses_wkt_when_generating_wkt() {
2765 let ts_msg = DescriptorProto {
2768 name: Some("Timestamp".into()),
2769 ..Default::default()
2770 };
2771 let files = [FileDescriptorProto {
2772 name: Some("google/protobuf/timestamp.proto".into()),
2773 package: Some("google.protobuf".into()),
2774 syntax: Some("proto3".into()),
2775 message_type: vec![ts_msg],
2776 ..Default::default()
2777 }];
2778 let config = CodeGenConfig::default();
2779 let ctx = CodeGenContext::for_generate(
2780 &files,
2781 &["google/protobuf/timestamp.proto".into()],
2782 &config,
2783 );
2784 assert_eq!(
2786 ctx.rust_type(".google.protobuf.Timestamp"),
2787 Some("google::protobuf::Timestamp")
2788 );
2789 }
2790
2791 #[test]
2794 fn test_matching_attributes_catchall() {
2795 let attrs = vec![(".".into(), "#[derive(Foo)]".into())];
2797 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
2798 assert!(result.to_string().contains("derive"));
2799 }
2800
2801 #[test]
2802 fn test_matching_attributes_exact_match() {
2803 let attrs = vec![(".my.pkg.MyMessage".into(), "#[derive(Bar)]".into())];
2804 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
2805 assert!(result.to_string().contains("derive"));
2806 }
2807
2808 #[test]
2809 fn test_matching_attributes_package_prefix() {
2810 let attrs = vec![(".my.pkg".into(), "#[derive(Baz)]".into())];
2811 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
2812 assert!(result.to_string().contains("derive"));
2813 }
2814
2815 #[test]
2816 fn test_matching_attributes_no_partial_segment_match() {
2817 let attrs = vec![(".my.pk".into(), "#[derive(Bad)]".into())];
2819 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
2820 assert!(result.is_empty());
2821 }
2822
2823 #[test]
2824 fn test_matching_attributes_no_match() {
2825 let attrs = vec![(".other.pkg".into(), "#[derive(Nope)]".into())];
2826 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
2827 assert!(result.is_empty());
2828 }
2829
2830 #[test]
2831 fn test_matching_attributes_multiple_accumulate() {
2832 let attrs = vec![
2834 (".".into(), "#[derive(A)]".into()),
2835 (".my.pkg".into(), "#[derive(B)]".into()),
2836 ];
2837 let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
2838 let s = result.to_string();
2839 assert!(s.contains("A") && s.contains("B"));
2840 }
2841
2842 #[test]
2843 fn test_matching_attributes_invalid_attr_errors() {
2844 let attrs = vec![(".".into(), "not valid {{{{".into())];
2847 let err = CodeGenContext::matching_attributes(&attrs, "my.pkg.Msg").unwrap_err();
2848 assert!(matches!(
2849 err,
2850 crate::CodeGenError::InvalidCustomAttribute { .. }
2851 ));
2852 }
2853
2854 #[test]
2855 fn test_matches_proto_prefix_catchall() {
2856 assert!(matches_proto_prefix(".", ".anything.here"));
2857 assert!(matches_proto_prefix(".", "."));
2858 }
2859
2860 #[test]
2861 fn test_matches_proto_prefix_segment_boundary() {
2862 assert!(!matches_proto_prefix(".my.pk", ".my.pkg.Msg"));
2864 assert!(matches_proto_prefix(".my.pkg", ".my.pkg.Msg"));
2866 }
2867}