use std::collections::{HashMap, HashSet};
use crate::features::{self, ResolvedFeatures};
use crate::generated::descriptor::{DescriptorProto, EnumDescriptorProto, FileDescriptorProto};
use crate::oneof::to_snake_case;
use crate::CodeGenConfig;
pub const SENTINEL_MOD: &str = "__buffa";
#[derive(Debug, Clone)]
pub struct SplitPath {
pub to_package: String,
pub within_package: String,
pub is_extern: bool,
}
pub struct CodeGenContext<'a> {
pub files: &'a [FileDescriptorProto],
pub config: &'a CodeGenConfig,
pub type_map: HashMap<String, String>,
package_of: HashMap<String, String>,
enum_closedness: HashMap<String, bool>,
comment_map: HashMap<String, String>,
nested_module_names: HashMap<String, String>,
unboxed_oneof_variants: HashSet<String>,
warnings: std::cell::RefCell<Vec<crate::CodeGenWarning>>,
}
fn child_package_segments(package: &str, all_packages: &HashSet<String>) -> HashSet<String> {
let prefix = if package.is_empty() {
String::new()
} else {
format!("{package}.")
};
all_packages
.iter()
.filter_map(|p| {
let rest = if package.is_empty() {
Some(p.as_str())
} else {
p.strip_prefix(&prefix)
};
rest.filter(|r| !r.is_empty())
.map(|r| r.split('.').next().unwrap_or(r).to_string())
})
.collect()
}
fn deconflict_package_modules(message_names: &[String], children: &HashSet<String>) -> Vec<String> {
let bases: Vec<String> = message_names.iter().map(|n| to_snake_case(n)).collect();
let mut taken: HashSet<String> = children.clone();
taken.insert(SENTINEL_MOD.to_string());
taken.extend(bases.iter().cloned());
let mut out = bases.clone();
let mut order: Vec<usize> = (0..bases.len()).collect();
order.sort_by(|&a, &b| bases[a].cmp(&bases[b]));
for i in order {
if !children.contains(&bases[i]) {
continue;
}
let mut candidate = format!("{}_", bases[i]);
while taken.contains(&candidate) {
candidate.push('_');
}
taken.insert(candidate.clone());
out[i] = candidate;
}
out
}
impl<'a> CodeGenContext<'a> {
pub fn new(
files: &'a [FileDescriptorProto],
config: &'a CodeGenConfig,
effective_extern_paths: &[(String, String)],
) -> Self {
Self::with_extern_resolution(files, config, effective_extern_paths, &[])
}
pub(crate) fn with_extern_resolution(
files: &'a [FileDescriptorProto],
config: &'a CodeGenConfig,
effective_extern_paths: &[(String, String)],
file_extern_paths: &[(String, String)],
) -> Self {
let mut type_map = HashMap::new();
let mut package_of = HashMap::new();
let mut enum_closedness = HashMap::new();
let mut comment_map = HashMap::new();
let mut nested_module_names = HashMap::new();
let unboxed_oneof_variants =
crate::oneof::resolve_unboxed_variants(files, &config.unboxed_oneof_fields);
let mut all_packages: HashSet<String> = HashSet::new();
let mut pkg_message_names: HashMap<String, Vec<String>> = HashMap::new();
for file in files {
let package = file.package.as_deref().unwrap_or("");
let is_extern = file
.name
.as_deref()
.and_then(|n| resolve_file_extern(n, file_extern_paths))
.is_some()
|| resolve_extern_prefix(package, effective_extern_paths).is_some();
if is_extern {
continue;
}
all_packages.insert(package.to_string());
for msg in &file.message_type {
if let Some(name) = &msg.name {
let fqn = if package.is_empty() {
format!(".{name}")
} else {
format!(".{package}.{name}")
};
if effective_extern_paths
.iter()
.any(|(proto, _)| proto == &fqn)
{
continue;
}
pkg_message_names
.entry(package.to_string())
.or_default()
.push(name.clone());
}
}
}
for (package, names) in &pkg_message_names {
let children = child_package_segments(package, &all_packages);
let modules = deconflict_package_modules(names, &children);
for (name, module) in names.iter().zip(modules) {
let fqn = if package.is_empty() {
format!(".{name}")
} else {
format!(".{package}.{name}")
};
nested_module_names.insert(fqn, module);
}
}
for file in files {
comment_map.extend(crate::comments::fqn_comments(file));
let package = file.package.as_deref().unwrap_or("");
let file_features = features::for_file(file);
let proto_prefix = if package.is_empty() {
String::from(".")
} else {
format!(".{}.", package)
};
let file_root = file
.name
.as_deref()
.and_then(|n| resolve_file_extern(n, file_extern_paths));
let local_module = package.replace('.', "::");
for msg in &file.message_type {
if let Some(name) = &msg.name {
let fqn = format!("{}{}", proto_prefix, name);
let (rust_path, is_extern) = resolve_type_path(
&fqn,
name,
file_root,
&local_module,
effective_extern_paths,
);
let parent_mod = if is_extern {
match rust_path.rsplit_once("::") {
Some((parent, _)) => format!("{parent}::{}", to_snake_case(name)),
None => to_snake_case(name),
}
} else {
let snake = nested_module_names
.get(&fqn)
.cloned()
.unwrap_or_else(|| to_snake_case(name));
join_mod(&local_module, &snake)
};
type_map.insert(fqn.clone(), rust_path);
package_of.insert(fqn.clone(), package.to_string());
register_nested_types(
&mut type_map,
&mut package_of,
package,
&fqn,
&parent_mod,
msg,
effective_extern_paths,
);
register_nested_enum_closedness(
&mut enum_closedness,
&fqn,
&file_features,
msg,
);
}
}
for enum_type in &file.enum_type {
if let Some(name) = &enum_type.name {
let fqn = format!("{}{}", proto_prefix, name);
let (rust_path, _) = resolve_type_path(
&fqn,
name,
file_root,
&local_module,
effective_extern_paths,
);
type_map.insert(fqn.clone(), rust_path);
package_of.insert(fqn.clone(), package.to_string());
register_enum_closedness(&mut enum_closedness, &fqn, &file_features, enum_type);
}
}
}
Self {
files,
config,
type_map,
package_of,
enum_closedness,
comment_map,
nested_module_names,
unboxed_oneof_variants,
warnings: std::cell::RefCell::new(Vec::new()),
}
}
pub(crate) fn warn(&self, warning: crate::CodeGenWarning) {
self.warnings.borrow_mut().push(warning);
}
pub(crate) fn take_warnings(&self) -> Vec<crate::CodeGenWarning> {
self.warnings.take()
}
pub fn nested_module_name(&self, package: &str, name: &str) -> String {
let fqn = if package.is_empty() {
format!(".{name}")
} else {
format!(".{package}.{name}")
};
self.nested_module_names
.get(&fqn)
.cloned()
.unwrap_or_else(|| to_snake_case(name))
}
pub fn for_generate(
files: &'a [FileDescriptorProto],
files_to_generate: &[String],
config: &'a CodeGenConfig,
) -> Self {
let paths = crate::effective_extern_paths(files, files_to_generate, config);
let file_paths = crate::effective_file_extern_paths(files_to_generate, config);
Self::with_extern_resolution(files, config, &paths, &file_paths)
}
pub fn rust_type(&self, proto_fqn: &str) -> Option<&str> {
self.type_map.get(proto_fqn).map(|s| s.as_str())
}
pub fn comment(&self, fqn: &str) -> Option<&str> {
self.comment_map.get(fqn).map(|s| s.as_str())
}
pub fn is_enum_closed(&self, proto_fqn: &str) -> Option<bool> {
self.enum_closedness.get(proto_fqn).copied()
}
pub fn rust_type_relative(
&self,
proto_fqn: &str,
current_package: &str,
nesting: usize,
) -> Option<String> {
let full_path = self.type_map.get(proto_fqn)?;
if full_path.starts_with("::") || full_path.starts_with("crate::") {
return Some(full_path.clone());
}
let target_package = self
.package_of
.get(proto_fqn)
.map(|s| s.as_str())
.unwrap_or("");
let target_rust_module = target_package.replace('.', "::");
let type_suffix = if target_rust_module.is_empty() {
full_path.as_str()
} else {
full_path
.strip_prefix(&format!("{}::", target_rust_module))
.unwrap_or(full_path)
};
if current_package == target_package {
if nesting == 0 {
return Some(type_suffix.to_string());
}
let supers = (0..nesting).map(|_| "super").collect::<Vec<_>>().join("::");
return Some(format!("{}::{}", supers, type_suffix));
}
let current_parts: Vec<&str> = if current_package.is_empty() {
vec![]
} else {
current_package.split('.').collect()
};
let target_parts: Vec<&str> = if target_package.is_empty() {
vec![]
} else {
target_package.split('.').collect()
};
let common_len = current_parts
.iter()
.zip(&target_parts)
.take_while(|(a, b)| a == b)
.count();
let up_count = (current_parts.len() - common_len) + nesting;
let down_parts = &target_parts[common_len..];
let mut segments: Vec<&str> = vec!["super"; up_count];
segments.extend_from_slice(down_parts);
let mut result = segments.join("::");
if !result.is_empty() {
result.push_str("::");
}
result.push_str(type_suffix);
Some(result)
}
pub fn rust_type_relative_split(
&self,
proto_fqn: &str,
current_package: &str,
nesting: usize,
) -> Option<SplitPath> {
let full_path = self.type_map.get(proto_fqn)?;
let target_package = self
.package_of
.get(proto_fqn)
.map(|s| s.as_str())
.unwrap_or("");
let target_rust_module = if full_path.starts_with("::") || full_path.starts_with("crate::")
{
let fqn_no_dot = proto_fqn.strip_prefix('.').unwrap_or(proto_fqn);
let within_proto = if target_package.is_empty() {
fqn_no_dot
} else {
fqn_no_dot
.strip_prefix(target_package)
.and_then(|s| s.strip_prefix('.'))
.unwrap_or(fqn_no_dot)
};
let within_segs = within_proto.split('.').count();
let full_segs: Vec<&str> = full_path.split("::").collect();
let cut = full_segs.len().saturating_sub(within_segs);
full_segs[..cut].join("::")
} else {
target_package.replace('.', "::")
};
let type_suffix = if target_rust_module.is_empty() {
full_path.as_str()
} else {
full_path
.strip_prefix(&format!("{}::", target_rust_module))
.unwrap_or(full_path)
};
if full_path.starts_with("::") || full_path.starts_with("crate::") {
return Some(SplitPath {
to_package: target_rust_module,
within_package: type_suffix.to_string(),
is_extern: true,
});
}
if current_package == target_package {
let to_package = if nesting == 0 {
String::new()
} else {
(0..nesting).map(|_| "super").collect::<Vec<_>>().join("::")
};
return Some(SplitPath {
to_package,
within_package: type_suffix.to_string(),
is_extern: false,
});
}
let current_parts: Vec<&str> = if current_package.is_empty() {
vec![]
} else {
current_package.split('.').collect()
};
let target_parts: Vec<&str> = if target_package.is_empty() {
vec![]
} else {
target_package.split('.').collect()
};
let common_len = current_parts
.iter()
.zip(&target_parts)
.take_while(|(a, b)| a == b)
.count();
let up_count = (current_parts.len() - common_len) + nesting;
let down_parts = &target_parts[common_len..];
let mut segments: Vec<&str> = vec!["super"; up_count];
segments.extend_from_slice(down_parts);
Some(SplitPath {
to_package: segments.join("::"),
within_package: type_suffix.to_string(),
is_extern: false,
})
}
pub(crate) fn matching_attributes(
attrs: &[(String, String)],
fqn: &str,
) -> Result<proc_macro2::TokenStream, crate::CodeGenError> {
if attrs.is_empty() {
return Ok(proc_macro2::TokenStream::new());
}
let fqn_dotted = format!(".{fqn}");
let mut tokens = proc_macro2::TokenStream::new();
for (prefix, attr_str) in attrs {
if matches_proto_prefix(prefix, &fqn_dotted) {
let parsed =
syn::parse_str::<proc_macro2::TokenStream>(attr_str).map_err(|err| {
crate::CodeGenError::InvalidCustomAttribute {
path: prefix.clone(),
attribute: attr_str.clone(),
detail: err.to_string(),
}
})?;
tokens.extend(parsed);
}
}
Ok(tokens)
}
pub fn use_bytes_type(&self, field_fqn: &str) -> bool {
self.config
.bytes_fields
.iter()
.any(|prefix| matches_proto_prefix(prefix, field_fqn))
}
pub fn oneof_unboxed(&self, variant_fqn: &str) -> bool {
self.unboxed_oneof_variants.contains(variant_fqn)
}
pub fn string_repr(&self, field_fqn: &str) -> crate::StringRepr {
self.config
.string_fields
.iter()
.rev()
.find(|(prefix, _)| matches_proto_prefix(prefix, field_fqn))
.map_or(crate::StringRepr::default(), |(_, repr)| *repr)
}
}
#[derive(Clone, Copy)]
pub(crate) struct MessageScope<'a> {
pub ctx: &'a CodeGenContext<'a>,
pub current_package: &'a str,
pub proto_fqn: &'a str,
pub features: &'a ResolvedFeatures,
pub nesting: usize,
}
impl<'a> MessageScope<'a> {
pub fn nested(&self, proto_fqn: &'a str, features: &'a ResolvedFeatures) -> MessageScope<'a> {
MessageScope {
ctx: self.ctx,
current_package: self.current_package,
proto_fqn,
features,
nesting: self.nesting + 1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AncillaryKind {
Oneof,
View,
ViewOneof,
}
impl AncillaryKind {
fn path_segments(self) -> &'static [&'static str] {
match self {
Self::Oneof => &["oneof"],
Self::View => &["view"],
Self::ViewOneof => &["view", "oneof"],
}
}
}
pub(crate) fn ancillary_prefix(
kind: AncillaryKind,
current_package: &str,
proto_fqn: &str,
from_nesting: usize,
) -> proc_macro2::TokenStream {
use crate::idents::make_field_ident;
use quote::quote;
debug_assert!(
!proto_fqn.starts_with('.'),
"ancillary_prefix expects dotless FQN, got {proto_fqn:?}"
);
let mut supers_tokens = proc_macro2::TokenStream::new();
for _ in 0..from_nesting {
supers_tokens.extend(quote! { super:: });
}
let sentinel = make_field_ident(SENTINEL_MOD);
let kind_segs: Vec<_> = kind
.path_segments()
.iter()
.map(|s| make_field_ident(s))
.collect();
let within_pkg = if current_package.is_empty() {
proto_fqn
} else {
proto_fqn
.strip_prefix(current_package)
.and_then(|s| s.strip_prefix('.'))
.unwrap_or(proto_fqn)
};
let msg_segs: Vec<_> = within_pkg
.split('.')
.filter(|s| !s.is_empty())
.map(|name| make_field_ident(&to_snake_case(name)))
.collect();
quote! { #supers_tokens #sentinel :: #(#kind_segs ::)* #(#msg_segs ::)* }
}
pub(crate) fn matches_proto_prefix(prefix: &str, fqn_dotted: &str) -> bool {
prefix == "."
|| prefix == fqn_dotted
|| (fqn_dotted.starts_with(prefix)
&& fqn_dotted.as_bytes().get(prefix.len()) == Some(&b'.'))
}
fn resolve_file_extern<'p>(
file_name: &str,
file_extern_paths: &'p [(String, String)],
) -> Option<&'p str> {
file_extern_paths
.iter()
.find(|(name, _)| name == file_name)
.map(|(_, rust)| rust.as_str())
}
pub(crate) fn resolve_extern_prefix(
package: &str,
extern_paths: &[(String, String)],
) -> Option<String> {
let dotted = format!(".{}", package);
let mut best: Option<(&str, &str, usize)> = None;
for (proto_prefix, rust_prefix) in extern_paths {
if dotted == *proto_prefix {
return Some(rust_prefix.clone());
}
if let Some(rest) = dotted.strip_prefix(proto_prefix.as_str()) {
if proto_prefix == "." || rest.starts_with('.') {
let prefix_len = proto_prefix.len();
if best.is_none_or(|(_, _, best_len)| prefix_len > best_len) {
best = Some((proto_prefix, rust_prefix, prefix_len));
}
}
}
}
let (proto_prefix, rust_prefix, _) = best?;
let rest = dotted.strip_prefix(proto_prefix)?;
let rest = rest.strip_prefix('.').unwrap_or(rest);
let suffix = rest
.split('.')
.map(to_snake_case)
.collect::<Vec<_>>()
.join("::");
Some(format!("{}::{}", rust_prefix, suffix))
}
fn resolve_extern_type(fqn: &str, extern_paths: &[(String, String)]) -> Option<String> {
if let Some((_, rust)) = extern_paths.iter().find(|(proto, _)| proto == fqn) {
return Some(rust.clone());
}
let mut best: Option<(&str, &str, usize)> = None;
for (proto_prefix, rust_prefix) in extern_paths {
let matches = proto_prefix == "."
|| fqn
.strip_prefix(proto_prefix.as_str())
.is_some_and(|rest| rest.starts_with('.'));
if matches && best.is_none_or(|(_, _, best_len)| proto_prefix.len() > best_len) {
best = Some((proto_prefix, rust_prefix, proto_prefix.len()));
}
}
let (proto_prefix, rust_prefix, _) = best?;
let rest = if proto_prefix == "." {
fqn.strip_prefix('.').unwrap_or(fqn)
} else {
fqn.strip_prefix(proto_prefix)
.and_then(|r| r.strip_prefix('.'))
.unwrap_or("")
};
let mut segments = rest.split('.').collect::<Vec<_>>();
let type_name = segments.pop()?;
let mut path = rust_prefix.to_string();
for module in segments {
path.push_str("::");
path.push_str(&to_snake_case(module));
}
path.push_str("::");
path.push_str(type_name);
Some(path)
}
fn join_mod(module: &str, name: &str) -> String {
if module.is_empty() {
name.to_string()
} else {
format!("{module}::{name}")
}
}
fn resolve_type_path(
fqn: &str,
name: &str,
file_root: Option<&str>,
local_module: &str,
extern_paths: &[(String, String)],
) -> (String, bool) {
if let Some((_, rust)) = extern_paths.iter().find(|(proto, _)| proto == fqn) {
(rust.clone(), true)
} else if let Some(root) = file_root {
(join_mod(root, name), true)
} else if let Some(path) = resolve_extern_type(fqn, extern_paths) {
(path, true)
} else {
(join_mod(local_module, name), false)
}
}
fn register_nested_types(
type_map: &mut HashMap<String, String>,
package_of: &mut HashMap<String, String>,
package: &str,
parent_fqn: &str,
parent_mod: &str,
msg: &crate::generated::descriptor::DescriptorProto,
extern_paths: &[(String, String)],
) {
for nested in &msg.nested_type {
if let Some(name) = &nested.name {
let fqn = format!("{}.{}", parent_fqn, name);
let (rust_path, child_mod) = match extern_paths.iter().find(|(proto, _)| proto == &fqn)
{
Some((_, rust)) => {
let child = match rust.rsplit_once("::") {
Some((parent, _)) => format!("{parent}::{}", to_snake_case(name)),
None => to_snake_case(name),
};
(rust.clone(), child)
}
None => (
format!("{parent_mod}::{name}"),
format!("{parent_mod}::{}", to_snake_case(name)),
),
};
type_map.insert(fqn.clone(), rust_path);
package_of.insert(fqn.clone(), package.to_string());
register_nested_types(
type_map,
package_of,
package,
&fqn,
&child_mod,
nested,
extern_paths,
);
}
}
for enum_type in &msg.enum_type {
if let Some(name) = &enum_type.name {
let fqn = format!("{}.{}", parent_fqn, name);
let rust_path = extern_paths
.iter()
.find(|(proto, _)| proto == &fqn)
.map(|(_, rust)| rust.clone())
.unwrap_or_else(|| format!("{parent_mod}::{name}"));
type_map.insert(fqn.clone(), rust_path);
package_of.insert(fqn, package.to_string());
}
}
}
fn register_enum_closedness(
map: &mut HashMap<String, bool>,
fqn: &str,
parent_features: &ResolvedFeatures,
enum_desc: &EnumDescriptorProto,
) {
let resolved = features::resolve_child(parent_features, features::enum_features(enum_desc));
let closed = resolved.enum_type == features::EnumType::Closed;
map.insert(fqn.to_string(), closed);
}
fn register_nested_enum_closedness(
map: &mut HashMap<String, bool>,
parent_fqn: &str,
parent_features: &ResolvedFeatures,
msg: &DescriptorProto,
) {
let msg_features = features::resolve_child(parent_features, features::message_features(msg));
for enum_type in &msg.enum_type {
if let Some(name) = &enum_type.name {
let fqn = format!("{}.{}", parent_fqn, name);
register_enum_closedness(map, &fqn, &msg_features, enum_type);
}
}
for nested in &msg.nested_type {
if let Some(name) = &nested.name {
let fqn = format!("{}.{}", parent_fqn, name);
register_nested_enum_closedness(map, &fqn, &msg_features, nested);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::generated::descriptor::{DescriptorProto, EnumDescriptorProto, FileDescriptorProto};
fn children(segs: &[&str]) -> HashSet<String> {
segs.iter().map(|s| s.to_string()).collect()
}
fn names(ns: &[&str]) -> Vec<String> {
ns.iter().map(|s| s.to_string()).collect()
}
#[test]
fn deconflict_no_collision_keeps_base() {
let out = deconflict_package_modules(&names(&["Oof", "Bar"]), &children(&["other"]));
assert_eq!(out, vec!["oof".to_string(), "bar".to_string()]);
}
#[test]
fn deconflict_single_collision_appends_underscore() {
let out = deconflict_package_modules(&names(&["Oof"]), &children(&["oof"]));
assert_eq!(out, vec!["oof_".to_string()]);
}
#[test]
fn deconflict_repeated_append_when_underscore_slot_also_taken() {
let out = deconflict_package_modules(&names(&["Oof"]), &children(&["oof", "oof_"]));
assert_eq!(out, vec!["oof__".to_string()]);
}
#[test]
fn deconflict_two_messages_racing_to_same_slot_stay_distinct() {
let out = deconflict_package_modules(&names(&["Oof", "Oof_"]), &children(&["oof", "oof_"]));
assert_eq!(out, vec!["oof__".to_string(), "oof___".to_string()]);
let set: HashSet<&String> = out.iter().collect();
assert_eq!(set.len(), out.len());
assert!(!out.contains(&"oof".to_string()) && !out.contains(&"oof_".to_string()));
}
#[test]
fn deconflict_is_independent_of_declaration_order() {
let ch = children(&["oof", "oof_"]);
let fwd = deconflict_package_modules(&names(&["Oof", "Oof_"]), &ch);
let rev = deconflict_package_modules(&names(&["Oof_", "Oof"]), &ch);
assert_eq!(fwd, vec!["oof__".to_string(), "oof___".to_string()]);
assert_eq!(rev, vec!["oof___".to_string(), "oof__".to_string()]);
}
#[test]
fn deconflict_avoids_other_messages_raw_base() {
let out = deconflict_package_modules(&names(&["Oof", "Oof_"]), &children(&["oof"]));
assert_eq!(out, vec!["oof__".to_string(), "oof_".to_string()]);
}
#[test]
fn deconflict_never_yields_the_sentinel() {
let out = deconflict_package_modules(&names(&["Buffa"]), &children(&["__buffa", "buffa"]));
assert_eq!(out, vec!["buffa_".to_string()]);
assert_ne!(out[0], SENTINEL_MOD);
}
#[test]
fn child_package_segments_extracts_immediate_segment() {
let pkgs = children(&["foo", "foo.oof", "foo.bar.baz", "foobar"]);
let mut got: Vec<String> = child_package_segments("foo", &pkgs).into_iter().collect();
got.sort();
assert_eq!(got, vec!["bar".to_string(), "oof".to_string()]);
}
fn make_file(
name: &str,
package: &str,
messages: Vec<DescriptorProto>,
enums: Vec<EnumDescriptorProto>,
) -> FileDescriptorProto {
FileDescriptorProto {
name: Some(name.to_string()),
package: if package.is_empty() {
None
} else {
Some(package.to_string())
},
message_type: messages,
enum_type: enums,
..Default::default()
}
}
fn msg(name: &str) -> DescriptorProto {
DescriptorProto {
name: Some(name.to_string()),
..Default::default()
}
}
fn msg_with_nested(name: &str, nested: Vec<DescriptorProto>) -> DescriptorProto {
DescriptorProto {
name: Some(name.to_string()),
nested_type: nested,
..Default::default()
}
}
fn msg_with_nested_and_enums(
name: &str,
nested: Vec<DescriptorProto>,
enums: Vec<EnumDescriptorProto>,
) -> DescriptorProto {
DescriptorProto {
name: Some(name.to_string()),
nested_type: nested,
enum_type: enums,
..Default::default()
}
}
fn enum_desc(name: &str) -> EnumDescriptorProto {
EnumDescriptorProto {
name: Some(name.to_string()),
..Default::default()
}
}
fn enum_with_closed_feature(name: &str) -> EnumDescriptorProto {
use crate::generated::descriptor::{feature_set, EnumOptions, FeatureSet};
EnumDescriptorProto {
name: Some(name.to_string()),
options: buffa::MessageField::some(EnumOptions {
features: buffa::MessageField::some(FeatureSet {
enum_type: Some(feature_set::EnumType::CLOSED),
..Default::default()
}),
..Default::default()
}),
..Default::default()
}
}
fn editions_file(
name: &str,
package: &str,
messages: Vec<DescriptorProto>,
enums: Vec<EnumDescriptorProto>,
) -> FileDescriptorProto {
use crate::generated::descriptor::Edition;
FileDescriptorProto {
name: Some(name.to_string()),
package: Some(package.to_string()),
syntax: Some("editions".to_string()),
edition: Some(Edition::EDITION_2023),
message_type: messages,
enum_type: enums,
..Default::default()
}
}
#[test]
fn test_message_with_package() {
let files = [make_file(
"test.proto",
"my.package",
vec![msg("Foo")],
vec![],
)];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.rust_type(".my.package.Foo"), Some("my::package::Foo"));
}
#[test]
fn test_message_no_package() {
let files = [make_file("test.proto", "", vec![msg("Bar")], vec![])];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.rust_type(".Bar"), Some("Bar"));
}
#[test]
fn test_nested_message_uses_module_path() {
let outer = msg_with_nested("Outer", vec![msg("Inner")]);
let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.rust_type(".pkg.Outer"), Some("pkg::Outer"));
assert_eq!(ctx.rust_type(".pkg.Outer.Inner"), Some("pkg::outer::Inner"));
}
#[test]
fn test_nested_message_no_package() {
let outer = msg_with_nested("Outer", vec![msg("Inner")]);
let files = [make_file("test.proto", "", vec![outer], vec![])];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.rust_type(".Outer"), Some("Outer"));
assert_eq!(ctx.rust_type(".Outer.Inner"), Some("outer::Inner"));
}
#[test]
fn test_deeply_nested_message() {
let deep = msg_with_nested("A", vec![msg_with_nested("B", vec![msg("C")])]);
let files = [make_file("test.proto", "pkg", vec![deep], vec![])];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.rust_type(".pkg.A"), Some("pkg::A"));
assert_eq!(ctx.rust_type(".pkg.A.B"), Some("pkg::a::B"));
assert_eq!(ctx.rust_type(".pkg.A.B.C"), Some("pkg::a::b::C"));
}
#[test]
fn test_nested_enum_uses_module_path() {
let outer = msg_with_nested_and_enums("Outer", vec![], vec![enum_desc("Status")]);
let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(
ctx.rust_type(".pkg.Outer.Status"),
Some("pkg::outer::Status")
);
}
#[test]
fn test_top_level_enum() {
let files = [make_file(
"test.proto",
"pkg",
vec![],
vec![enum_desc("Status")],
)];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.rust_type(".pkg.Status"), Some("pkg::Status"));
}
#[test]
fn test_same_named_nested_types_in_different_parents_are_distinct() {
let outer1 = msg_with_nested("Outer1", vec![msg("Inner")]);
let outer2 = msg_with_nested("Outer2", vec![msg("Inner")]);
let files = [make_file("a.proto", "pkg", vec![outer1, outer2], vec![])];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(
ctx.rust_type(".pkg.Outer1.Inner"),
Some("pkg::outer1::Inner")
);
assert_eq!(
ctx.rust_type(".pkg.Outer2.Inner"),
Some("pkg::outer2::Inner")
);
assert_ne!(
ctx.rust_type(".pkg.Outer1.Inner"),
ctx.rust_type(".pkg.Outer2.Inner")
);
}
#[test]
fn test_multiple_files() {
let files = [
make_file("a.proto", "ns.a", vec![msg("MsgA")], vec![]),
make_file("b.proto", "ns.b", vec![msg("MsgB")], vec![]),
];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.rust_type(".ns.a.MsgA"), Some("ns::a::MsgA"));
assert_eq!(ctx.rust_type(".ns.b.MsgB"), Some("ns::b::MsgB"));
}
#[test]
fn test_extern_path_exact_per_type_match() {
let files = [make_file(
"test.proto",
"test.pkg",
vec![msg("Msg")],
vec![],
)];
let config = CodeGenConfig {
extern_paths: vec![(".test.pkg.Msg".into(), "::ext_crate::Msg".into())],
..Default::default()
};
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.rust_type(".test.pkg.Msg"), Some("::ext_crate::Msg"));
}
#[test]
fn test_extern_path_per_type_overrides_package_prefix() {
let files = [make_file(
"test.proto",
"test.pkg",
vec![msg("Msg"), msg("Other")],
vec![],
)];
let config = CodeGenConfig {
extern_paths: vec![
(".test.pkg".into(), "::pkg_crate".into()),
(".test.pkg.Msg".into(), "::ext_crate::Msg".into()),
],
..Default::default()
};
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.rust_type(".test.pkg.Msg"), Some("::ext_crate::Msg"));
assert_eq!(ctx.rust_type(".test.pkg.Other"), Some("::pkg_crate::Other"));
}
#[test]
fn test_extern_path_nested_type_inherits_per_type_override() {
let outer = msg_with_nested("Outer", vec![msg("Inner")]);
let files = [make_file("test.proto", "test.pkg", vec![outer], vec![])];
let config = CodeGenConfig {
extern_paths: vec![(".test.pkg.Outer".into(), "::ext::Outer".into())],
..Default::default()
};
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.rust_type(".test.pkg.Outer"), Some("::ext::Outer"));
assert_eq!(
ctx.rust_type(".test.pkg.Outer.Inner"),
Some("::ext::outer::Inner")
);
}
#[test]
fn test_extern_path_exact_per_type_enum() {
let files = [make_file(
"test.proto",
"test.pkg",
vec![],
vec![enum_desc("Status")],
)];
let config = CodeGenConfig {
extern_paths: vec![(".test.pkg.Status".into(), "::ext_crate::Status".into())],
..Default::default()
};
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(
ctx.rust_type(".test.pkg.Status"),
Some("::ext_crate::Status")
);
}
#[test]
fn test_extern_path_package_prefix_still_resolves() {
let files = [make_file(
"test.proto",
"test.pkg",
vec![msg("Msg")],
vec![],
)];
let config = CodeGenConfig {
extern_paths: vec![(".test.pkg".into(), "::pkg_crate".into())],
..Default::default()
};
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.rust_type(".test.pkg.Msg"), Some("::pkg_crate::Msg"));
}
#[test]
fn test_extern_path_per_type_does_not_affect_unmapped_type() {
let files = [make_file(
"test.proto",
"test.pkg",
vec![msg("Msg"), msg("Other")],
vec![],
)];
let config = CodeGenConfig {
extern_paths: vec![(".test.pkg.Msg".into(), "::ext_crate::Msg".into())],
..Default::default()
};
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.rust_type(".test.pkg.Msg"), Some("::ext_crate::Msg"));
assert_eq!(ctx.rust_type(".test.pkg.Other"), Some("test::pkg::Other"));
}
#[test]
fn test_keyword_package_segment_in_type_map() {
let files = [make_file(
"latlng.proto",
"google.type",
vec![msg("LatLng")],
vec![],
)];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(
ctx.rust_type(".google.type.LatLng"),
Some("google::type::LatLng")
);
}
#[test]
fn test_keyword_package_relative_same_package() {
let files = [make_file(
"latlng.proto",
"google.type",
vec![msg("LatLng"), msg("Expr")],
vec![],
)];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(
ctx.rust_type_relative(".google.type.LatLng", "google.type", 0),
Some("LatLng".into())
);
}
#[test]
fn test_keyword_package_cross_package() {
let files = [
make_file("latlng.proto", "google.type", vec![msg("LatLng")], vec![]),
make_file("svc.proto", "google.cloud", vec![msg("Service")], vec![]),
];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(
ctx.rust_type_relative(".google.type.LatLng", "google.cloud", 0),
Some("super::type::LatLng".into())
);
}
#[test]
fn test_keyword_nested_message_module() {
let outer = msg_with_nested("Type", vec![msg("Inner")]);
let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.rust_type(".pkg.Type"), Some("pkg::Type"));
assert_eq!(ctx.rust_type(".pkg.Type.Inner"), Some("pkg::type::Inner"));
}
#[test]
fn test_unknown_type_returns_none() {
let files = [make_file("test.proto", "pkg", vec![msg("Foo")], vec![])];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.rust_type(".pkg.Unknown"), None);
}
#[test]
fn test_relative_same_package_top_level() {
let files = [make_file("a.proto", "pkg", vec![msg("Foo")], vec![])];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(
ctx.rust_type_relative(".pkg.Foo", "pkg", 0),
Some("Foo".into())
);
}
#[test]
fn test_relative_cross_package() {
let files = [
make_file("a.proto", "pkg_a", vec![msg("Foo")], vec![]),
make_file("b.proto", "pkg_b", vec![msg("Bar")], vec![]),
];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(
ctx.rust_type_relative(".pkg_a.Foo", "pkg_b", 0),
Some("super::pkg_a::Foo".into())
);
}
#[test]
fn test_relative_no_package() {
let files = [make_file("a.proto", "", vec![msg("Foo")], vec![])];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.rust_type_relative(".Foo", "", 0), Some("Foo".into()));
}
#[test]
fn test_relative_unknown_returns_none() {
let files = [make_file("a.proto", "pkg", vec![msg("Foo")], vec![])];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.rust_type_relative(".pkg.Unknown", "pkg", 0), None);
}
#[test]
fn test_relative_dotted_package() {
let files = [make_file("a.proto", "my.pkg", vec![msg("Foo")], vec![])];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(
ctx.rust_type_relative(".my.pkg.Foo", "my.pkg", 0),
Some("Foo".into())
);
}
#[test]
fn test_relative_cross_dotted_packages() {
let files = [
make_file(
"timestamp.proto",
"google.protobuf",
vec![msg("Timestamp")],
vec![],
),
make_file(
"test.proto",
"protobuf_test_messages.proto3",
vec![msg("TestAllTypesProto3")],
vec![],
),
];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(
ctx.rust_type_relative(
".google.protobuf.Timestamp",
"protobuf_test_messages.proto3",
0,
),
Some("super::super::google::protobuf::Timestamp".into())
);
}
#[test]
fn test_relative_nested_type_from_same_package() {
let outer = msg_with_nested("Outer", vec![msg("Inner")]);
let files = [make_file("test.proto", "pkg", vec![outer], vec![])];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(
ctx.rust_type_relative(".pkg.Outer.Inner", "pkg", 0),
Some("outer::Inner".into())
);
}
#[test]
fn test_relative_shared_prefix_not_confused() {
let files = [
make_file("ab.proto", "a.b", vec![msg("Msg1")], vec![]),
make_file("abc.proto", "a.bc", vec![msg("Msg2")], vec![]),
];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(
ctx.rust_type_relative(".a.b.Msg1", "a.bc", 0),
Some("super::b::Msg1".into())
);
assert_eq!(
ctx.rust_type_relative(".a.bc.Msg2", "a.b", 0),
Some("super::bc::Msg2".into())
);
}
#[test]
fn test_relative_cross_package_nesting_1() {
let outer = msg_with_nested_and_enums("Business", vec![], vec![enum_desc("Status")]);
let files = [
make_file("admin.proto", "a.b.admin.v1", vec![msg("Svc")], vec![]),
make_file("biz.proto", "a.b.v1", vec![outer], vec![]),
];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(
ctx.rust_type_relative(".a.b.v1.Business.Status", "a.b.admin.v1", 0),
Some("super::super::v1::business::Status".into())
);
assert_eq!(
ctx.rust_type_relative(".a.b.v1.Business.Status", "a.b.admin.v1", 1),
Some("super::super::super::v1::business::Status".into())
);
}
#[test]
fn test_relative_same_package_nesting_1() {
let files = [make_file(
"test.proto",
"pkg",
vec![msg("Foo"), msg("Bar")],
vec![],
)];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(
ctx.rust_type_relative(".pkg.Foo", "pkg", 0),
Some("Foo".into())
);
assert_eq!(
ctx.rust_type_relative(".pkg.Foo", "pkg", 1),
Some("super::Foo".into())
);
assert_eq!(
ctx.rust_type_relative(".pkg.Foo", "pkg", 2),
Some("super::super::Foo".into())
);
}
#[test]
fn test_resolve_file_extern_exact_match_only() {
let mappings = [(
"google/protobuf/descriptor.proto".to_string(),
"::buffa_descriptor::generated::descriptor".to_string(),
)];
assert_eq!(
resolve_file_extern("google/protobuf/descriptor.proto", &mappings),
Some("::buffa_descriptor::generated::descriptor"),
);
assert_eq!(
resolve_file_extern("google/protobuf/timestamp.proto", &mappings),
None,
);
assert_eq!(
resolve_file_extern("vendor/google/protobuf/descriptor.proto", &mappings),
None,
);
}
#[test]
fn test_resolve_extern_prefix_exact_match() {
let result = resolve_extern_prefix(
"my.common",
&[(".my.common".into(), "::common_protos".into())],
);
assert_eq!(result, Some("::common_protos".into()));
}
#[test]
fn test_resolve_extern_prefix_sub_package() {
let result = resolve_extern_prefix(
"my.common.sub",
&[(".my.common".into(), "::common_protos".into())],
);
assert_eq!(result, Some("::common_protos::sub".into()));
}
#[test]
fn test_resolve_extern_prefix_no_match() {
let result = resolve_extern_prefix(
"other.pkg",
&[(".my.common".into(), "::common_protos".into())],
);
assert_eq!(result, None);
}
#[test]
fn test_resolve_extern_prefix_partial_name_no_match() {
let result = resolve_extern_prefix(
"my.commonext",
&[(".my.common".into(), "::common_protos".into())],
);
assert_eq!(result, None);
}
#[test]
fn test_resolve_extern_prefix_longest_match_wins() {
let result = resolve_extern_prefix(
"my.common.sub",
&[
(".my".into(), "::crate_a".into()),
(".my.common".into(), "::crate_b".into()),
],
);
assert_eq!(result, Some("::crate_b::sub".into()));
}
#[test]
fn test_resolve_extern_prefix_catchall() {
let result = resolve_extern_prefix("greet.v1", &[(".".into(), "crate::proto".into())]);
assert_eq!(result, Some("crate::proto::greet::v1".into()));
}
#[test]
fn test_resolve_extern_prefix_catchall_empty_pkg() {
let result = resolve_extern_prefix("", &[(".".into(), "crate::proto".into())]);
assert_eq!(result, Some("crate::proto".into()));
}
#[test]
fn test_resolve_extern_prefix_catchall_longest_wins() {
let result = resolve_extern_prefix(
"google.protobuf",
&[
(".".into(), "crate::proto".into()),
(
".google.protobuf".into(),
"::buffa_types::google::protobuf".into(),
),
],
);
assert_eq!(result, Some("::buffa_types::google::protobuf".into()));
}
#[test]
fn test_resolve_extern_prefix_catchall_keyword_package() {
let result = resolve_extern_prefix("google.type", &[(".".into(), "crate::proto".into())]);
assert_eq!(result, Some("crate::proto::google::type".into()));
}
#[test]
fn test_resolve_extern_type_exact_match() {
let result = resolve_extern_type(
".google.protobuf.Timestamp",
&[(
".google.protobuf.Timestamp".into(),
"::pbjson_types::Timestamp".into(),
)],
);
assert_eq!(result, Some("::pbjson_types::Timestamp".into()));
}
#[test]
fn test_resolve_extern_type_exact_wins_over_prefix() {
let result = resolve_extern_type(
".my.pkg.Msg",
&[
(".my.pkg".into(), "::pkg_crate".into()),
(".my.pkg.Msg".into(), "::ext_crate::Msg".into()),
],
);
assert_eq!(result, Some("::ext_crate::Msg".into()));
}
#[test]
fn test_resolve_extern_type_package_prefix_appends_type() {
let result = resolve_extern_type(
".my.common.sub.Msg",
&[(".my.common".into(), "::common_protos".into())],
);
assert_eq!(result, Some("::common_protos::sub::Msg".into()));
}
#[test]
fn test_resolve_extern_type_catchall() {
let result = resolve_extern_type(".greet.v1.Hello", &[(".".into(), "crate::proto".into())]);
assert_eq!(result, Some("crate::proto::greet::v1::Hello".into()));
}
#[test]
fn test_resolve_extern_type_no_match() {
let result = resolve_extern_type(
".other.pkg.Msg",
&[(".my.common".into(), "::common_protos".into())],
);
assert_eq!(result, None);
}
#[test]
fn test_resolve_extern_type_partial_name_no_match() {
let result = resolve_extern_type(
".my.commonext.Msg",
&[(".my.common".into(), "::common_protos".into())],
);
assert_eq!(result, None);
}
#[test]
fn test_split_extern_top_level() {
let outer = msg_with_nested("Value", vec![msg("Inner")]);
let files = [make_file(
"struct.proto",
"google.protobuf",
vec![outer],
vec![],
)];
let config = CodeGenConfig::default();
let extern_paths = vec![(
".google.protobuf".into(),
"::buffa_types::google::protobuf".into(),
)];
let ctx = CodeGenContext::new(&files, &config, &extern_paths);
let split = ctx
.rust_type_relative_split(".google.protobuf.Value", "my.pkg", 3)
.expect("type resolves");
assert!(split.is_extern);
assert_eq!(split.to_package, "::buffa_types::google::protobuf");
assert_eq!(split.within_package, "Value");
}
#[test]
fn test_split_extern_nested_type() {
let outer = msg_with_nested("Value", vec![msg("Inner")]);
let files = [make_file(
"struct.proto",
"google.protobuf",
vec![outer],
vec![],
)];
let config = CodeGenConfig::default();
let extern_paths = vec![(
".google.protobuf".into(),
"::buffa_types::google::protobuf".into(),
)];
let ctx = CodeGenContext::new(&files, &config, &extern_paths);
let split = ctx
.rust_type_relative_split(".google.protobuf.Value.Inner", "my.pkg", 0)
.expect("nested type resolves");
assert!(split.is_extern);
assert_eq!(split.to_package, "::buffa_types::google::protobuf");
assert_eq!(split.within_package, "value::Inner");
}
#[test]
fn test_split_per_type_extern_override() {
let outer = msg_with_nested("Outer", vec![msg("Inner")]);
let files = [make_file("custom.proto", "my.pkg", vec![outer], vec![])];
let config = CodeGenConfig {
extern_paths: vec![(".my.pkg.Outer".into(), "::ext::custom::Outer".into())],
..Default::default()
};
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
let split = ctx
.rust_type_relative_split(".my.pkg.Outer", "other.pkg", 2)
.expect("overridden type resolves");
assert!(split.is_extern);
assert_eq!(split.to_package, "::ext::custom");
assert_eq!(split.within_package, "Outer");
let nested = ctx
.rust_type_relative_split(".my.pkg.Outer.Inner", "other.pkg", 0)
.expect("nested type resolves");
assert!(nested.is_extern);
assert_eq!(nested.to_package, "::ext::custom");
assert_eq!(nested.within_package, "outer::Inner");
}
#[test]
fn test_extern_path_top_level_message() {
let files = [make_file(
"common.proto",
"my.common",
vec![msg("SharedMsg")],
vec![],
)];
let config = CodeGenConfig {
extern_paths: vec![(".my.common".into(), "::common_protos".into())],
..Default::default()
};
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(
ctx.rust_type(".my.common.SharedMsg"),
Some("::common_protos::SharedMsg")
);
}
#[test]
fn test_extern_path_nested_message() {
let files = [make_file(
"common.proto",
"my.common",
vec![msg_with_nested("Outer", vec![msg("Inner")])],
vec![],
)];
let config = CodeGenConfig {
extern_paths: vec![(".my.common".into(), "::common_protos".into())],
..Default::default()
};
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(
ctx.rust_type(".my.common.Outer"),
Some("::common_protos::Outer")
);
assert_eq!(
ctx.rust_type(".my.common.Outer.Inner"),
Some("::common_protos::outer::Inner")
);
}
#[test]
fn test_extern_path_enum() {
let files = [make_file(
"common.proto",
"my.common",
vec![],
vec![enum_desc("Status")],
)];
let config = CodeGenConfig {
extern_paths: vec![(".my.common".into(), "::common_protos".into())],
..Default::default()
};
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(
ctx.rust_type(".my.common.Status"),
Some("::common_protos::Status")
);
}
#[test]
fn test_extern_path_does_not_affect_other_packages() {
let files = [
make_file("common.proto", "my.common", vec![msg("SharedMsg")], vec![]),
make_file(
"service.proto",
"my.service",
vec![msg("MyService")],
vec![],
),
];
let config = CodeGenConfig {
extern_paths: vec![(".my.common".into(), "::common_protos".into())],
..Default::default()
};
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(
ctx.rust_type(".my.common.SharedMsg"),
Some("::common_protos::SharedMsg")
);
assert_eq!(
ctx.rust_type(".my.service.MyService"),
Some("my::service::MyService")
);
}
#[test]
fn test_extern_path_relative_returns_absolute() {
let files = [
make_file("common.proto", "my.common", vec![msg("SharedMsg")], vec![]),
make_file(
"service.proto",
"my.service",
vec![msg("MyService")],
vec![],
),
];
let config = CodeGenConfig {
extern_paths: vec![(".my.common".into(), "::common_protos".into())],
..Default::default()
};
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(
ctx.rust_type_relative(".my.common.SharedMsg", "my.service", 0),
Some("::common_protos::SharedMsg".into())
);
}
#[test]
fn test_is_enum_closed_proto3_default_open() {
let files = [make_file("a.proto", "p", vec![], vec![enum_desc("E")])];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.is_enum_closed(".p.E"), Some(true));
}
#[test]
fn test_is_enum_closed_editions_default_open() {
let files = [editions_file("a.proto", "p", vec![], vec![enum_desc("E")])];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.is_enum_closed(".p.E"), Some(false));
}
#[test]
fn test_is_enum_closed_per_enum_override() {
let files = [editions_file(
"a.proto",
"p",
vec![],
vec![enum_desc("Open"), enum_with_closed_feature("Closed")],
)];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.is_enum_closed(".p.Open"), Some(false));
assert_eq!(ctx.is_enum_closed(".p.Closed"), Some(true));
}
#[test]
fn test_is_enum_closed_nested_per_enum_override() {
let files = [editions_file(
"a.proto",
"p",
vec![msg_with_nested_and_enums(
"M",
vec![],
vec![enum_with_closed_feature("Inner")],
)],
vec![],
)];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.is_enum_closed(".p.M.Inner"), Some(true));
}
#[test]
fn test_is_enum_closed_unknown_enum_returns_none() {
let files = [editions_file("a.proto", "p", vec![], vec![])];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::new(&files, &config, &config.extern_paths);
assert_eq!(ctx.is_enum_closed(".other.Unknown"), None);
}
#[test]
fn test_for_generate_auto_injects_wkt_mapping() {
let ts_msg = DescriptorProto {
name: Some("Timestamp".into()),
..Default::default()
};
let files = [FileDescriptorProto {
name: Some("google/protobuf/timestamp.proto".into()),
package: Some("google.protobuf".into()),
syntax: Some("proto3".into()),
message_type: vec![ts_msg],
..Default::default()
}];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::for_generate(&files, &["other.proto".into()], &config);
assert_eq!(
ctx.rust_type(".google.protobuf.Timestamp"),
Some("::buffa_types::google::protobuf::Timestamp"),
"WKT auto-mapping must be applied via for_generate"
);
}
#[test]
fn test_for_generate_suppresses_wkt_when_generating_wkt() {
let ts_msg = DescriptorProto {
name: Some("Timestamp".into()),
..Default::default()
};
let files = [FileDescriptorProto {
name: Some("google/protobuf/timestamp.proto".into()),
package: Some("google.protobuf".into()),
syntax: Some("proto3".into()),
message_type: vec![ts_msg],
..Default::default()
}];
let config = CodeGenConfig::default();
let ctx = CodeGenContext::for_generate(
&files,
&["google/protobuf/timestamp.proto".into()],
&config,
);
assert_eq!(
ctx.rust_type(".google.protobuf.Timestamp"),
Some("google::protobuf::Timestamp")
);
}
#[test]
fn test_matching_attributes_catchall() {
let attrs = vec![(".".into(), "#[derive(Foo)]".into())];
let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
assert!(result.to_string().contains("derive"));
}
#[test]
fn test_matching_attributes_exact_match() {
let attrs = vec![(".my.pkg.MyMessage".into(), "#[derive(Bar)]".into())];
let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
assert!(result.to_string().contains("derive"));
}
#[test]
fn test_matching_attributes_package_prefix() {
let attrs = vec![(".my.pkg".into(), "#[derive(Baz)]".into())];
let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
assert!(result.to_string().contains("derive"));
}
#[test]
fn test_matching_attributes_no_partial_segment_match() {
let attrs = vec![(".my.pk".into(), "#[derive(Bad)]".into())];
let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
assert!(result.is_empty());
}
#[test]
fn test_matching_attributes_no_match() {
let attrs = vec![(".other.pkg".into(), "#[derive(Nope)]".into())];
let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
assert!(result.is_empty());
}
#[test]
fn test_matching_attributes_multiple_accumulate() {
let attrs = vec![
(".".into(), "#[derive(A)]".into()),
(".my.pkg".into(), "#[derive(B)]".into()),
];
let result = CodeGenContext::matching_attributes(&attrs, "my.pkg.MyMessage").unwrap();
let s = result.to_string();
assert!(s.contains("A") && s.contains("B"));
}
#[test]
fn test_matching_attributes_invalid_attr_errors() {
let attrs = vec![(".".into(), "not valid {{{{".into())];
let err = CodeGenContext::matching_attributes(&attrs, "my.pkg.Msg").unwrap_err();
assert!(matches!(
err,
crate::CodeGenError::InvalidCustomAttribute { .. }
));
}
#[test]
fn test_matches_proto_prefix_catchall() {
assert!(matches_proto_prefix(".", ".anything.here"));
assert!(matches_proto_prefix(".", "."));
}
#[test]
fn test_matches_proto_prefix_segment_boundary() {
assert!(!matches_proto_prefix(".my.pk", ".my.pkg.Msg"));
assert!(matches_proto_prefix(".my.pkg", ".my.pkg.Msg"));
}
}