pub(crate) mod comments;
pub mod context;
pub(crate) mod defaults;
pub(crate) mod enumeration;
pub(crate) mod extension;
pub(crate) mod feature_gates;
pub use feature_gates::FeatureGateNames;
pub(crate) mod features;
#[doc(hidden)]
pub use buffa_descriptor::generated;
pub mod idents;
pub(crate) mod impl_message;
pub(crate) mod impl_text;
pub(crate) mod imports;
pub(crate) mod lazy_view;
pub(crate) mod message;
pub(crate) mod oneof;
pub(crate) mod owned_view;
pub(crate) mod reflect;
pub(crate) mod reflect_owned;
pub(crate) mod reflect_view;
pub(crate) mod view;
use crate::generated::descriptor::FileDescriptorProto;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
pub const ALLOW_LINTS: &[&str] = &[
"non_camel_case_types",
"dead_code",
"unused_imports",
"unused_qualifications",
"clippy::derivable_impls",
"clippy::match_single_binding",
"clippy::uninlined_format_args",
"clippy::doc_lazy_continuation",
"clippy::module_inception",
];
pub fn allow_lints_attr() -> TokenStream {
let lints: Vec<TokenStream> = ALLOW_LINTS
.iter()
.map(|l| syn::parse_str(l).expect("lint name parses as path"))
.collect();
quote! { #[allow( #(#lints),* )] }
}
#[derive(Debug)]
pub struct GeneratedFile {
pub name: String,
pub package: String,
pub kind: GeneratedFileKind,
pub content: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum GeneratedFileKind {
Owned,
View,
LazyView,
Oneof,
ViewOneof,
Ext,
PackageMod,
Companion,
}
pub(crate) fn parse_custom_type_path(path: &str) -> Result<proc_macro2::TokenStream, CodeGenError> {
let ty: syn::Type =
syn::parse_str(path).map_err(|_| CodeGenError::InvalidTypePath(path.to_string()))?;
Ok(quote::quote! { #ty })
}
pub(crate) fn parse_custom_map_path(path: &str) -> Result<proc_macro2::TokenStream, CodeGenError> {
let ty: syn::Type = syn::parse_str(path).map_err(|_| {
CodeGenError::InvalidTypePath(format!(
"{path} (map custom path takes no `<K, V>` parameters and no `*` placeholder)"
))
})?;
let syn::Type::Path(tp) = &ty else {
return Err(CodeGenError::InvalidTypePath(format!(
"{path} (map custom path must be a plain type path)"
)));
};
if tp
.path
.segments
.iter()
.any(|s| !matches!(s.arguments, syn::PathArguments::None))
{
return Err(CodeGenError::InvalidTypePath(format!(
"{path} (map custom path must not include `<K, V>`; the key and value are appended automatically)"
)));
}
Ok(quote::quote! { #ty })
}
pub(crate) fn parse_wildcard_type_path(
template: &str,
inner: &proc_macro2::TokenStream,
) -> Result<proc_macro2::TokenStream, CodeGenError> {
if !template.contains('*') {
return Err(CodeGenError::MissingWildcard(template.to_string()));
}
let substituted = template.replace('*', &inner.to_string());
let ty: syn::Type = syn::parse_str(&substituted)
.map_err(|_| CodeGenError::InvalidTypePath(format!("{template} (as {substituted})")))?;
Ok(quote::quote! { #ty })
}
pub(crate) fn parse_custom_list_path(
template: &str,
elem: &proc_macro2::TokenStream,
) -> Result<proc_macro2::TokenStream, CodeGenError> {
if !template.contains('*') {
return Err(CodeGenError::MissingListPlaceholder(template.to_string()));
}
let substituted = template.replace('*', &elem.to_string());
let ty: syn::Type = syn::parse_str(&substituted)
.map_err(|_| CodeGenError::InvalidTypePath(template.to_string()))?;
Ok(quote::quote! { #ty })
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum StringRepr {
#[default]
String,
Custom(String),
}
impl StringRepr {
pub(crate) fn type_path(
&self,
resolver: &imports::ImportResolver,
ctx: &context::CodeGenContext,
nesting: usize,
) -> Result<proc_macro2::TokenStream, CodeGenError> {
match self {
StringRepr::String => Ok(resolver.string_at(ctx, nesting)),
StringRepr::Custom(path) => parse_custom_type_path(path),
}
}
pub(crate) fn is_default(&self) -> bool {
matches!(self, StringRepr::String)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum BytesRepr {
#[default]
Vec,
Bytes,
Custom(String),
}
impl BytesRepr {
pub(crate) fn type_path(
&self,
resolver: &imports::ImportResolver,
ctx: &context::CodeGenContext,
nesting: usize,
) -> Result<proc_macro2::TokenStream, CodeGenError> {
use quote::quote;
match self {
BytesRepr::Vec => {
let vec = resolver.vec_at(ctx, nesting);
Ok(quote! { #vec<u8> })
}
BytesRepr::Bytes => Ok(quote! { ::buffa::bytes::Bytes }),
BytesRepr::Custom(path) => parse_custom_type_path(path),
}
}
pub(crate) fn is_default(&self) -> bool {
matches!(self, BytesRepr::Vec)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum MapRepr {
#[default]
HashMap,
BTreeMap,
Custom(String),
}
impl MapRepr {
pub(crate) fn type_path(
&self,
key: &proc_macro2::TokenStream,
value: &proc_macro2::TokenStream,
resolver: &imports::ImportResolver,
ctx: &context::CodeGenContext,
nesting: usize,
) -> Result<proc_macro2::TokenStream, CodeGenError> {
use quote::quote;
match self {
MapRepr::HashMap => {
let hm = resolver.hashmap_at(ctx, nesting);
Ok(quote! { #hm<#key, #value> })
}
MapRepr::BTreeMap => Ok(quote! { ::buffa::alloc::collections::BTreeMap<#key, #value> }),
MapRepr::Custom(path) => {
let ty = parse_custom_map_path(path)?;
Ok(quote! { #ty<#key, #value> })
}
}
}
pub(crate) fn is_default(&self) -> bool {
matches!(self, MapRepr::HashMap)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum PointerRepr {
#[default]
Box,
Custom(String),
}
impl PointerRepr {
pub(crate) fn type_path(
&self,
message_field: &proc_macro2::TokenStream,
inner: &proc_macro2::TokenStream,
) -> Result<proc_macro2::TokenStream, CodeGenError> {
use quote::quote;
match self {
PointerRepr::Box => Ok(quote! { #message_field<#inner> }),
PointerRepr::Custom(template) => {
let ptr = parse_wildcard_type_path(template, inner)?;
Ok(quote! { #message_field<#inner, #ptr> })
}
}
}
pub(crate) fn some_path(
&self,
inner: &proc_macro2::TokenStream,
) -> Result<proc_macro2::TokenStream, CodeGenError> {
use quote::quote;
match self {
PointerRepr::Box => Ok(quote! { ::buffa::MessageField::<#inner> }),
PointerRepr::Custom(template) => {
let ptr = parse_wildcard_type_path(template, inner)?;
Ok(quote! { ::buffa::MessageField::<#inner, #ptr> })
}
}
}
pub(crate) fn pointer_type(
&self,
inner: &proc_macro2::TokenStream,
) -> Result<proc_macro2::TokenStream, CodeGenError> {
use quote::quote;
match self {
PointerRepr::Box => Ok(quote! { ::buffa::alloc::boxed::Box<#inner> }),
PointerRepr::Custom(template) => parse_wildcard_type_path(template, inner),
}
}
pub(crate) fn pointer_new(
&self,
inner: &proc_macro2::TokenStream,
value: &proc_macro2::TokenStream,
) -> Result<proc_macro2::TokenStream, CodeGenError> {
use quote::quote;
match self {
PointerRepr::Box => Ok(quote! { ::buffa::alloc::boxed::Box::new(#value) }),
PointerRepr::Custom(template) => {
let ptr = parse_wildcard_type_path(template, inner)?;
Ok(quote! { <#ptr as ::buffa::ProtoBox<#inner>>::new(#value) })
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum RepeatedRepr {
#[default]
Vec,
Custom(String),
}
impl RepeatedRepr {
pub(crate) fn type_path(
&self,
elem: &proc_macro2::TokenStream,
resolver: &imports::ImportResolver,
ctx: &context::CodeGenContext,
nesting: usize,
) -> Result<proc_macro2::TokenStream, CodeGenError> {
use quote::quote;
match self {
RepeatedRepr::Vec => {
let vec = resolver.vec_at(ctx, nesting);
Ok(quote! { #vec<#elem> })
}
RepeatedRepr::Custom(template) => parse_custom_list_path(template, elem),
}
}
pub(crate) fn is_default(&self) -> bool {
matches!(self, RepeatedRepr::Vec)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ReflectMode {
#[default]
Off,
Bridge,
VTable,
}
impl ReflectMode {
pub fn apply(self, config: &mut CodeGenConfig) {
let (reflection, vtable) = match self {
ReflectMode::Off => (false, false),
ReflectMode::Bridge => (true, false),
ReflectMode::VTable => (true, true),
};
config.generate_reflection = reflection;
config.generate_reflection_vtable = vtable;
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct CodeGenConfig {
pub generate_views: bool,
pub lazy_views: bool,
pub preserve_unknown_fields: bool,
pub generate_json: bool,
pub generate_arbitrary: bool,
pub extern_paths: Vec<(String, String)>,
pub bytes_fields: Vec<(String, BytesRepr)>,
pub string_fields: Vec<(String, StringRepr)>,
pub map_fields: Vec<(String, MapRepr)>,
pub pointer_fields: Vec<(String, PointerRepr)>,
pub repeated_fields: Vec<(String, RepeatedRepr)>,
pub unboxed_oneof_fields: Vec<String>,
pub strict_utf8_mapping: bool,
pub allow_message_set: bool,
pub generate_text: bool,
pub emit_register_fn: bool,
pub file_per_package: bool,
pub type_attributes: Vec<(String, String)>,
pub field_attributes: Vec<(String, String)>,
pub message_attributes: Vec<(String, String)>,
pub enum_attributes: Vec<(String, String)>,
pub oneof_attributes: Vec<(String, String)>,
pub gate_impls_on_crate_features: bool,
pub generate_with_setters: bool,
pub generate_reflection: bool,
pub generate_reflection_vtable: bool,
pub gate_reflect_on_crate_feature: bool,
pub idiomatic_enum_aliases: bool,
pub idiomatic_imports: bool,
pub feature_gate_names: FeatureGateNames,
pub type_name_prefix: String,
}
impl Default for CodeGenConfig {
fn default() -> Self {
Self {
generate_views: true,
lazy_views: false,
preserve_unknown_fields: true,
generate_json: false,
generate_arbitrary: false,
extern_paths: Vec::new(),
bytes_fields: Vec::new(),
string_fields: Vec::new(),
map_fields: Vec::new(),
pointer_fields: Vec::new(),
repeated_fields: Vec::new(),
unboxed_oneof_fields: Vec::new(),
strict_utf8_mapping: false,
allow_message_set: false,
generate_text: false,
emit_register_fn: true,
file_per_package: false,
type_attributes: Vec::new(),
field_attributes: Vec::new(),
message_attributes: Vec::new(),
enum_attributes: Vec::new(),
oneof_attributes: Vec::new(),
gate_impls_on_crate_features: false,
generate_with_setters: true,
generate_reflection: false,
generate_reflection_vtable: false,
gate_reflect_on_crate_feature: false,
idiomatic_enum_aliases: true,
idiomatic_imports: false,
feature_gate_names: FeatureGateNames::default(),
type_name_prefix: String::new(),
}
}
}
impl CodeGenConfig {
pub(crate) fn feature_gates(&self) -> feature_gates::FeatureGates<'_> {
feature_gates::FeatureGates::for_config(self)
}
pub(crate) fn prefixed_type_name(&self, proto_name: &str) -> String {
format!("{}{proto_name}", self.type_name_prefix)
}
pub(crate) fn validate_type_name_prefix(&self) -> Result<(), CodeGenError> {
let prefix = &self.type_name_prefix;
let valid = prefix.is_empty()
|| (prefix.starts_with(|c: char| c.is_ascii_uppercase())
&& prefix.chars().all(|c| c.is_ascii_alphanumeric()));
if valid {
Ok(())
} else {
Err(CodeGenError::InvalidTypeNamePrefix {
prefix: prefix.clone(),
})
}
}
}
pub(crate) fn effective_extern_paths(
file_descriptors: &[FileDescriptorProto],
files_to_generate: &[String],
config: &CodeGenConfig,
) -> Vec<(String, String)> {
let mut paths = config.extern_paths.clone();
let has_wkt_mapping = paths.iter().any(|(proto, _)| proto == ".google.protobuf");
if !has_wkt_mapping {
let generating_wkts = file_descriptors
.iter()
.filter(|fd| {
fd.name
.as_deref()
.is_some_and(|n| files_to_generate.iter().any(|f| f == n))
})
.any(|fd| fd.package.as_deref() == Some("google.protobuf"));
if !generating_wkts {
paths.push((
".google.protobuf".to_string(),
"::buffa_types::google::protobuf".to_string(),
));
}
}
paths
}
pub(crate) fn effective_file_extern_paths(
files_to_generate: &[String],
config: &CodeGenConfig,
) -> Vec<(String, String)> {
const DESCRIPTOR_FILES: [(&str, &str, &str); 2] = [
(
"google/protobuf/descriptor.proto",
"google.protobuf",
"::buffa_descriptor::generated::descriptor",
),
(
"google/protobuf/compiler/plugin.proto",
"google.protobuf.compiler",
"::buffa_descriptor::generated::compiler",
),
];
DESCRIPTOR_FILES
.into_iter()
.filter(|(proto_file, package, _)| {
if context::resolve_extern_prefix(package, &config.extern_paths).is_some() {
return false;
}
!files_to_generate.iter().any(|f| f == proto_file)
})
.map(|(proto_file, _, rust_module)| (proto_file.to_string(), rust_module.to_string()))
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct AliasConflict {
pub camel_target: String,
pub proto_values: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CodeGenWarning {
#[non_exhaustive]
IdiomaticAliasesSuppressed {
enum_name: String,
conflicts: Vec<AliasConflict>,
invalid: Vec<String>,
},
#[non_exhaustive]
OwnedViewAccessorSuppressed {
wrapper_name: String,
field_name: String,
},
#[non_exhaustive]
LazyViewsRequireViews,
}
impl core::fmt::Display for CodeGenWarning {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::IdiomaticAliasesSuppressed {
enum_name,
conflicts,
invalid,
} => {
let cause = match (conflicts.is_empty(), invalid.is_empty()) {
(false, true) => "naming conflict",
(true, false) => "invalid identifier",
_ => "naming conflict / invalid identifier",
};
write!(
f,
"enum `{enum_name}`: idiomatic CamelCase aliases suppressed ({cause})"
)?;
let mut parts: Vec<String> = conflicts
.iter()
.map(|c| format!("{} → {}", c.proto_values.join(", "), c.camel_target))
.collect();
parts.extend(invalid.iter().map(|n| format!("{n} → invalid identifier")));
if !parts.is_empty() {
write!(f, ": {}", parts.join("; "))?;
}
Ok(())
}
Self::OwnedViewAccessorSuppressed {
wrapper_name,
field_name,
} => {
write!(
f,
"`{wrapper_name}`: accessor for field `{field_name}` suppressed \
(collides with a reserved wrapper method); use `.view().{field_name}` instead"
)
}
Self::LazyViewsRequireViews => {
write!(
f,
"lazy_views requires generate_views (the lazy family reuses the \
eager view-oneof enums and sub-view types); no lazy views were \
generated — enable generate_views (buffa-build: \
`.generate_views(true)`, the default; plugin: `views=true`)"
)
}
}
}
}
pub fn generate(
file_descriptors: &[FileDescriptorProto],
files_to_generate: &[String],
config: &CodeGenConfig,
) -> Result<Vec<GeneratedFile>, CodeGenError> {
Ok(generate_with_diagnostics(file_descriptors, files_to_generate, config)?.0)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum CustomElemKind {
String,
Bytes,
}
#[derive(Default)]
struct CustomElements {
elements: std::collections::BTreeMap<String, CustomElemKind>,
map_keys: std::collections::BTreeSet<String>,
}
fn collect_custom_elements(
ctx: &context::CodeGenContext,
file_descriptors: &[FileDescriptorProto],
files_to_generate: &[String],
) -> CustomElements {
use crate::generated::descriptor::field_descriptor_proto::{Label, Type};
fn walk(
ctx: &context::CodeGenContext,
messages: &[crate::generated::descriptor::DescriptorProto],
scope: &str,
parent_features: &crate::features::ResolvedFeatures,
out: &mut CustomElements,
) {
for msg in messages {
let name = msg.name.as_deref().unwrap_or("");
let fqn = if scope.is_empty() {
name.to_string()
} else {
format!("{scope}.{name}")
};
let msg_features = crate::features::resolve_child(
parent_features,
crate::features::message_features(msg),
);
for field in &msg.field {
if field.label.unwrap_or_default() != Label::LABEL_REPEATED {
continue;
}
let field_name = field.name.as_deref().unwrap_or("");
let field_fqn = format!(".{fqn}.{field_name}");
if let Some(entry) = crate::message::find_map_entry(msg, field) {
let key_ty = crate::message::map_entry_key_type(ctx, entry, &msg_features);
let val_ty = crate::message::map_entry_value_type(ctx, entry, &msg_features);
if let crate::BytesRepr::Custom(path) =
crate::impl_message::map_value_bytes_repr(
ctx, key_ty, val_ty, &fqn, field_name,
)
{
out.elements.entry(path).or_insert(CustomElemKind::Bytes);
}
if let crate::StringRepr::Custom(path) = ctx.string_repr(&field_fqn) {
if key_ty == Some(Type::TYPE_STRING) {
out.map_keys.insert(path.clone());
}
if val_ty == Some(Type::TYPE_STRING) {
out.elements.entry(path).or_insert(CustomElemKind::String);
}
}
continue;
}
let field_features = crate::features::resolve_field(ctx, field, &msg_features);
let ty = crate::impl_message::effective_type(ctx, field, &field_features);
match ty {
Type::TYPE_STRING => {
if let crate::StringRepr::Custom(path) = ctx.string_repr(&field_fqn) {
out.elements.entry(path).or_insert(CustomElemKind::String);
}
}
Type::TYPE_BYTES => {
if let crate::BytesRepr::Custom(path) = ctx.bytes_repr(&field_fqn) {
out.elements.entry(path).or_insert(CustomElemKind::Bytes);
}
}
_ => {}
}
}
walk(ctx, &msg.nested_type, &fqn, &msg_features, out);
}
}
let mut out = CustomElements::default();
for file_name in files_to_generate {
let Some(file) = file_descriptors
.iter()
.find(|f| f.name.as_deref() == Some(file_name.as_str()))
else {
continue;
};
let pkg = file.package.as_deref().unwrap_or("");
let file_features = crate::features::for_file(file);
walk(ctx, &file.message_type, pkg, &file_features, &mut out);
}
out
}
fn render_custom_elem_impls(
ctx: &context::CodeGenContext,
elems: &CustomElements,
) -> Result<TokenStream, CodeGenError> {
let json_gate = ctx.config.feature_gates().json;
let reflect_gate = ctx.config.feature_gates().reflect;
let mut out = TokenStream::new();
for (path, kind) in &elems.elements {
let ty = parse_custom_type_path(path)?;
if ctx.config.generate_json && *kind == CustomElemKind::Bytes {
out.extend(feature_gates::cfg_block(
quote! {
impl ::buffa::json_helpers::ProtoElemJson for #ty {
fn serialize_proto_json<S: ::serde::Serializer>(
v: &Self,
s: S,
) -> ::core::result::Result<S::Ok, S::Error> {
::buffa::json_helpers::bytes::serialize(
::core::convert::AsRef::<[u8]>::as_ref(v),
s,
)
}
fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>(
d: D,
) -> ::core::result::Result<Self, D::Error> {
::buffa::json_helpers::bytes::deserialize(d)
}
}
},
json_gate,
));
}
if ctx.config.generate_reflection_vtable {
let value_ref = match kind {
CustomElemKind::String => quote! {
::buffa_descriptor::reflect::ValueRef::String(
::core::convert::AsRef::<str>::as_ref(self),
)
},
CustomElemKind::Bytes => quote! {
::buffa_descriptor::reflect::ValueRef::Bytes(
::core::convert::AsRef::<[u8]>::as_ref(self),
)
},
};
out.extend(feature_gates::cfg_block(
quote! {
impl ::buffa_descriptor::reflect::ReflectElement for #ty {
fn as_value_ref(&self) -> ::buffa_descriptor::reflect::ValueRef<'_> {
#value_ref
}
}
},
reflect_gate,
));
}
}
if ctx.config.generate_reflection_vtable {
for path in &elems.map_keys {
let ty = parse_custom_type_path(path)?;
out.extend(feature_gates::cfg_block(
quote! {
impl ::buffa_descriptor::reflect::ReflectMapKey for #ty {
fn as_map_key_ref(&self) -> ::buffa_descriptor::reflect::MapKeyRef<'_> {
::buffa_descriptor::reflect::MapKeyRef::String(
::core::convert::AsRef::<str>::as_ref(self),
)
}
}
},
reflect_gate,
));
}
}
Ok(out)
}
pub fn generate_with_diagnostics(
file_descriptors: &[FileDescriptorProto],
files_to_generate: &[String],
config: &CodeGenConfig,
) -> Result<(Vec<GeneratedFile>, Vec<CodeGenWarning>), CodeGenError> {
if config.generate_reflection_vtable && !config.generate_reflection {
return Err(CodeGenError::Other(
"generate_reflection_vtable requires generate_reflection to be enabled \
(it provides the descriptor pool the reflect impls resolve against)"
.into(),
));
}
if config.idiomatic_imports && !config.file_per_package {
return Err(CodeGenError::Other(
"idiomatic_imports requires file_per_package to be enabled (the multi-file \
layout include!-merges every proto's content into the shared package root, \
where emitted `use` directives could collide across files)"
.into(),
));
}
if let Err((kind, name)) = config.feature_gates().validate() {
return Err(CodeGenError::Other(format!(
"invalid {kind} feature-gate name {name:?}: a Cargo feature name starts \
with an ASCII alphanumeric or '_' and contains only alphanumerics, \
'_', '-', '+', or '.'; an invalid name would leave the emitted \
#[cfg(feature = ...)] permanently false, silently compiling the \
gated impls away"
)));
}
config.validate_type_name_prefix()?;
let ctx = context::CodeGenContext::for_generate(file_descriptors, files_to_generate, config);
if config.lazy_views && !config.generate_views {
ctx.warn(CodeGenWarning::LazyViewsRequireViews);
}
let mut by_package: std::collections::BTreeMap<String, Vec<&FileDescriptorProto>> =
std::collections::BTreeMap::new();
for file_name in files_to_generate {
let file_desc = file_descriptors
.iter()
.find(|f| f.name.as_deref() == Some(file_name.as_str()))
.ok_or_else(|| CodeGenError::FileNotFound(file_name.clone()))?;
let pkg = file_desc.package.as_deref().unwrap_or("").to_string();
by_package.entry(pkg).or_default().push(file_desc);
}
let fds_bytes = if config.generate_reflection {
reflect::encode_fds_once(file_descriptors)
} else {
Vec::new()
};
let custom_elems = collect_custom_elements(&ctx, file_descriptors, files_to_generate);
let custom_elem_impls = render_custom_elem_impls(&ctx, &custom_elems)?;
let empty_impls = TokenStream::new();
let mut output = Vec::new();
let mut custom_emitted = false;
for (package, files) in by_package {
let impls = if custom_emitted {
&empty_impls
} else {
custom_emitted = true;
&custom_elem_impls
};
generate_package(&ctx, &package, &files, &fds_bytes, impls, &mut output)?;
}
Ok((output, ctx.take_warnings()))
}
pub fn generate_module_tree<F: AsRef<str>, P: AsRef<str>>(
entries: &[(F, P)],
include_mode: IncludeMode<'_>,
emit_inner_allow: bool,
) -> String {
use std::collections::BTreeMap;
use std::fmt::Write;
use crate::idents::escape_mod_ident;
#[derive(Default)]
struct ModNode {
files: Vec<String>,
children: BTreeMap<String, Self>,
}
let mut root = ModNode::default();
for (file_name, package) in entries {
let package = package.as_ref();
let pkg_parts: Vec<&str> = if package.is_empty() {
vec![]
} else {
package.split('.').collect()
};
let mut node = &mut root;
for seg in &pkg_parts {
node = node.children.entry(seg.to_string()).or_default();
}
node.files.push(file_name.as_ref().to_string());
}
let lints = ALLOW_LINTS.join(", ");
let mut out = String::new();
let _ = writeln!(out, "// @generated by buffa-codegen. DO NOT EDIT.");
if emit_inner_allow {
let _ = writeln!(out, "#![allow({lints})]");
}
let _ = writeln!(out);
fn emit(out: &mut String, node: &ModNode, depth: usize, mode: IncludeMode<'_>, lints: &str) {
let indent = " ".repeat(depth);
for file in &node.files {
match mode {
IncludeMode::Relative(prefix) => {
let _ = writeln!(out, r#"{indent}include!("{prefix}{file}");"#);
}
IncludeMode::OutDir => {
let _ = writeln!(
out,
r#"{indent}include!(concat!(env!("OUT_DIR"), "/{file}"));"#
);
}
}
}
for (name, child) in &node.children {
let escaped = escape_mod_ident(name);
let _ = writeln!(out, "{indent}#[allow({lints})]");
let _ = writeln!(out, "{indent}pub mod {escaped} {{");
let _ = writeln!(out, "{indent} use super::*;");
emit(out, child, depth + 1, mode, lints);
let _ = writeln!(out, "{indent}}}");
}
}
emit(&mut out, &root, 0, include_mode, &lints);
out
}
#[derive(Debug, Clone, Copy)]
pub enum IncludeMode<'a> {
Relative(&'a str),
OutDir,
}
fn validate_file(file: &FileDescriptorProto) -> Result<(), CodeGenError> {
use std::collections::HashMap;
let sentinel = context::SENTINEL_MOD;
let package = file.package.as_deref().unwrap_or("");
if package.split('.').any(|seg| seg == sentinel) {
return Err(CodeGenError::ReservedModuleName {
name: sentinel.to_string(),
location: format!("package '{package}'"),
});
}
for enum_type in &file.enum_type {
let name = enum_type.name.as_deref().unwrap_or("");
if name == sentinel {
return Err(CodeGenError::ReservedModuleName {
name: sentinel.to_string(),
location: format!("enum '{package}.{name}'"),
});
}
}
fn walk(
messages: &[crate::generated::descriptor::DescriptorProto],
scope: &str,
sentinel: &str,
) -> Result<(), CodeGenError> {
let mut seen: HashMap<String, &str> = HashMap::new();
for msg in messages {
let name = msg.name.as_deref().unwrap_or("");
let fqn = if scope.is_empty() {
name.to_string()
} else {
format!("{scope}.{name}")
};
for field in &msg.field {
if let Some(fname) = &field.name {
if fname.starts_with("__buffa_") {
return Err(CodeGenError::ReservedFieldName {
message_name: fqn,
field_name: fname.clone(),
});
}
}
}
let module_name = crate::oneof::to_snake_case(name);
if module_name == sentinel {
return Err(CodeGenError::ReservedModuleName {
name: sentinel.to_string(),
location: format!("message '{fqn}'"),
});
}
if let Some(existing) = seen.get(&module_name) {
return Err(CodeGenError::ModuleNameConflict {
scope: scope.to_string(),
name_a: existing.to_string(),
name_b: name.to_string(),
module_name,
});
}
seen.insert(module_name, name);
walk(&msg.nested_type, &fqn, sentinel)?;
}
Ok(())
}
walk(&file.message_type, package, sentinel)
}
struct ProtoContent {
stem: String,
owned: TokenStream,
view: TokenStream,
lazy_view: TokenStream,
oneof: TokenStream,
view_oneof: TokenStream,
ext: TokenStream,
root_reexports: Vec<message::ReexportCandidate>,
}
fn generate_proto_content(
ctx: &context::CodeGenContext,
current_package: &str,
file: &FileDescriptorProto,
reg: &mut message::RegistryPaths,
) -> Result<ProtoContent, CodeGenError> {
use crate::idents::make_field_ident;
use crate::message::MessageOutput;
validate_file(file)?;
let resolver = imports::ImportResolver::new();
let features = crate::features::for_file(file);
let mut owned = TokenStream::new();
let mut view = TokenStream::new();
let mut lazy_view = TokenStream::new();
let mut oneof = TokenStream::new();
let mut view_oneof = TokenStream::new();
let mut ext = TokenStream::new();
let mut root_reexports: Vec<message::ReexportCandidate> = Vec::new();
let sentinel = make_field_ident(context::SENTINEL_MOD);
for enum_type in &file.enum_type {
let enum_proto_name = enum_type.name.as_deref().unwrap_or("");
let enum_rust_name = ctx.config.prefixed_type_name(enum_proto_name);
let enum_fqn = if current_package.is_empty() {
enum_proto_name.to_string()
} else {
format!("{}.{}", current_package, enum_proto_name)
};
owned.extend(enumeration::generate_enum(
ctx,
enum_type,
&enum_rust_name,
&enum_fqn,
&features,
&resolver,
)?);
}
for message_type in &file.message_type {
let top_level_name = message_type.name.as_deref().unwrap_or("");
let rust_name = ctx.config.prefixed_type_name(top_level_name);
let proto_fqn = if current_package.is_empty() {
top_level_name.to_string()
} else {
format!("{}.{}", current_package, top_level_name)
};
let MessageOutput {
owned_top,
owned_mod,
oneof_tree: msg_oneof,
view_tree: msg_view,
lazy_view_tree: msg_lazy_view,
view_oneof_tree: msg_view_oneof,
reg: msg_reg,
} = message::generate_message(
ctx,
message_type,
current_package,
&rust_name,
&proto_fqn,
&features,
&resolver,
)?;
owned.extend(owned_top);
let mod_name = ctx.nested_module_name(current_package, top_level_name);
let mod_ident = make_field_ident(&mod_name);
let mod_doc = if mod_name == crate::oneof::to_snake_case(top_level_name) {
quote! {}
} else {
let doc = format!(
"Nested items of `{top_level_name}`. The module name carries a \
trailing `_` to avoid a collision with another module in this \
scope (a sub-package or sibling message of the same name). See \
buffa#135."
);
quote! { #[doc = #doc] }
};
for p in msg_reg.json_ext {
reg.json_ext.push(quote! { #mod_ident :: #p });
}
for p in msg_reg.text_ext {
reg.text_ext.push(quote! { #mod_ident :: #p });
}
reg.json_any.extend(msg_reg.json_any);
reg.text_any.extend(msg_reg.text_any);
if !owned_mod.is_empty() {
owned.extend(quote! {
#mod_doc
pub mod #mod_ident {
#[allow(unused_imports)]
use super::*;
#owned_mod
}
});
}
oneof.extend(msg_oneof);
view.extend(msg_view);
lazy_view.extend(msg_lazy_view);
view_oneof.extend(msg_view_oneof);
if ctx.config.generate_views {
let view_ident = format_ident!("{rust_name}View");
root_reexports.push(message::ReexportCandidate {
name: view_ident.to_string(),
tokens: feature_gates::cfg_block(
quote! {
#[doc(inline)]
pub use self :: #sentinel :: view :: #view_ident;
},
ctx.config.feature_gates().views,
),
});
let owned_view_ident = format_ident!("{rust_name}OwnedView");
root_reexports.push(message::ReexportCandidate {
name: owned_view_ident.to_string(),
tokens: feature_gates::cfg_block(
quote! {
#[doc(inline)]
pub use self :: #sentinel :: view :: #owned_view_ident;
},
ctx.config.feature_gates().views,
),
});
if ctx.config.lazy_views {
let lazy_ident = format_ident!("{rust_name}LazyView");
root_reexports.push(message::ReexportCandidate {
name: lazy_ident.to_string(),
tokens: feature_gates::cfg_block(
quote! {
#[doc(inline)]
pub use self :: #sentinel :: lazy_view :: #lazy_ident;
},
ctx.config.feature_gates().views,
),
});
}
}
}
let (file_ext_tokens, file_ext_json, file_ext_text) = extension::generate_extensions(
ctx,
&file.extension,
current_package,
2,
&features,
current_package,
)?;
ext.extend(file_ext_tokens);
for id in file_ext_json {
reg.json_ext.push(quote! { #sentinel :: ext :: #id });
}
for id in file_ext_text {
reg.text_ext.push(quote! { #sentinel :: ext :: #id });
}
for ext_field in &file.extension {
let const_ident = extension::extension_const_ident(ext_field.name.as_deref().unwrap_or(""));
root_reexports.push(message::ReexportCandidate {
name: const_ident.to_string(),
tokens: quote! {
#[doc(inline)]
pub use self :: #sentinel :: ext :: #const_ident;
},
});
}
Ok(ProtoContent {
stem: proto_path_to_stem(file.name.as_deref().unwrap_or("")),
owned,
view,
lazy_view,
oneof,
view_oneof,
ext,
root_reexports,
})
}
#[derive(Default)]
struct PackageSections {
owned: Vec<TokenStream>,
view: Vec<TokenStream>,
lazy_view: Vec<TokenStream>,
oneof: Vec<TokenStream>,
view_oneof: Vec<TokenStream>,
ext: Vec<TokenStream>,
}
impl PackageSections {
fn push_inline(&mut self, pc: ProtoContent) {
let push_if_nonempty = |dst: &mut Vec<TokenStream>, ts: TokenStream| {
if !ts.is_empty() {
dst.push(ts);
}
};
push_if_nonempty(&mut self.owned, pc.owned);
push_if_nonempty(&mut self.view, pc.view);
push_if_nonempty(&mut self.lazy_view, pc.lazy_view);
push_if_nonempty(&mut self.oneof, pc.oneof);
push_if_nonempty(&mut self.view_oneof, pc.view_oneof);
push_if_nonempty(&mut self.ext, pc.ext);
}
}
fn generate_package(
ctx: &context::CodeGenContext,
current_package: &str,
files: &[&FileDescriptorProto],
fds_bytes: &[u8],
custom_elem_impls: &TokenStream,
out: &mut Vec<GeneratedFile>,
) -> Result<(), CodeGenError> {
let mut reg = message::RegistryPaths::default();
let mut root_reexports: Vec<message::ReexportCandidate> = Vec::new();
if ctx.config.idiomatic_imports && ctx.config.file_per_package {
ctx.imports_begin_collecting();
let warn_mark = ctx.warnings_len();
let mut scratch_reg = message::RegistryPaths::default();
let mut occupied = root_occupied_names(ctx, files);
for file in files {
let pc = generate_proto_content(ctx, current_package, file, &mut scratch_reg)?;
occupied.extend(pc.root_reexports.into_iter().map(|c| c.name));
}
ctx.truncate_warnings(warn_mark);
occupied.insert("register_types".to_string());
if ctx.config.generate_reflection {
occupied.insert("descriptor_pool".to_string());
}
let collected = ctx.imports_take_collected();
ctx.imports_set_resolving(imports::RootImports::assign(&collected, &occupied));
}
let sections = if ctx.config.file_per_package {
let mut sections = PackageSections::default();
for file in files {
let mut pc = generate_proto_content(ctx, current_package, file, &mut reg)?;
root_reexports.append(&mut pc.root_reexports);
sections.push_inline(pc);
}
sections
} else {
let mut sections = PackageSections::default();
for file in files {
let mut pc = generate_proto_content(ctx, current_package, file, &mut reg)?;
root_reexports.append(&mut pc.root_reexports);
let source = file.name.as_deref().unwrap_or("");
let stem = pc.stem;
let emit = |suffix: &str,
kind: GeneratedFileKind,
tokens: TokenStream,
section: &mut Vec<TokenStream>,
out: &mut Vec<GeneratedFile>|
-> Result<(), CodeGenError> {
if tokens.is_empty() {
return Ok(());
}
let name = format!("{stem}{suffix}.rs");
section.push(quote! { include!(#name); });
out.push(GeneratedFile {
name,
package: current_package.to_string(),
kind,
content: format_tokens(tokens, source)?,
});
Ok(())
};
emit(
"",
GeneratedFileKind::Owned,
pc.owned,
&mut sections.owned,
out,
)?;
emit(
".__view",
GeneratedFileKind::View,
pc.view,
&mut sections.view,
out,
)?;
emit(
".__lazy_view",
GeneratedFileKind::LazyView,
pc.lazy_view,
&mut sections.lazy_view,
out,
)?;
emit(
".__oneof",
GeneratedFileKind::Oneof,
pc.oneof,
&mut sections.oneof,
out,
)?;
emit(
".__view_oneof",
GeneratedFileKind::ViewOneof,
pc.view_oneof,
&mut sections.view_oneof,
out,
)?;
emit(
".__ext",
GeneratedFileKind::Ext,
pc.ext,
&mut sections.ext,
out,
)?;
}
sections
};
let reexport_block = surviving_root_reexports(ctx, files, ®, root_reexports);
out.push(GeneratedFile {
name: if ctx.config.file_per_package {
package_to_filename(current_package)
} else {
package_to_mod_filename(current_package)
},
package: current_package.to_string(),
kind: GeneratedFileKind::PackageMod,
content: generate_package_mod(
ctx,
§ions,
®,
&reexport_block,
fds_bytes,
custom_elem_impls,
)?,
});
ctx.imports_reset();
Ok(())
}
fn root_occupied_names(
ctx: &context::CodeGenContext,
files: &[&FileDescriptorProto],
) -> std::collections::BTreeSet<String> {
let mut occupied = std::collections::BTreeSet::new();
occupied.insert(context::SENTINEL_MOD.to_string());
for file in files {
let package = file.package.as_deref().unwrap_or("");
for m in &file.message_type {
let name = m.name.as_deref().unwrap_or("");
occupied.insert(ctx.config.prefixed_type_name(name));
occupied.insert(ctx.nested_module_name(package, name));
}
for e in &file.enum_type {
occupied.insert(
ctx.config
.prefixed_type_name(e.name.as_deref().unwrap_or("")),
);
}
}
occupied
}
fn surviving_root_reexports(
ctx: &context::CodeGenContext,
files: &[&FileDescriptorProto],
reg: &message::RegistryPaths,
mut candidates: Vec<message::ReexportCandidate>,
) -> TokenStream {
use crate::idents::make_field_ident;
let occupied = root_occupied_names(ctx, files);
if ctx.config.emit_register_fn && !reg.is_empty() {
let sentinel = make_field_ident(context::SENTINEL_MOD);
let json_or_text = ctx.config.feature_gates().json_or_text();
candidates.push(message::ReexportCandidate {
name: "register_types".to_string(),
tokens: feature_gates::cfg_block_any(
quote! {
#[doc(inline)]
pub use self :: #sentinel :: register_types;
},
&json_or_text,
),
});
}
message::emit_surviving_reexports(candidates, &occupied)
}
fn generate_package_mod(
ctx: &context::CodeGenContext,
sections: &PackageSections,
reg: &message::RegistryPaths,
root_reexports: &TokenStream,
fds_bytes: &[u8],
custom_elem_impls: &TokenStream,
) -> Result<String, CodeGenError> {
use crate::idents::make_field_ident;
let owned = §ions.owned;
let view = §ions.view;
let lazy_view = §ions.lazy_view;
let view_oneof = §ions.view_oneof;
let oneof = §ions.oneof;
let ext = §ions.ext;
let view_oneof_mod = if !view_oneof.is_empty() {
quote! {
pub mod oneof {
#[allow(unused_imports)]
use super::*;
#(#view_oneof)*
}
}
} else {
TokenStream::new()
};
debug_assert!(view_oneof.is_empty() || !view.is_empty());
let view_mod = if ctx.config.generate_views && !view.is_empty() {
feature_gates::cfg_block(
quote! {
pub mod view {
#[allow(unused_imports)]
use super::*;
#(#view)*
#view_oneof_mod
}
},
ctx.config.feature_gates().views,
)
} else {
TokenStream::new()
};
debug_assert!(lazy_view.is_empty() || !view.is_empty());
let lazy_view_mod = if !lazy_view.is_empty() {
feature_gates::cfg_block(
quote! {
pub mod lazy_view {
#[allow(unused_imports)]
use super::*;
#(#lazy_view)*
}
},
ctx.config.feature_gates().views,
)
} else {
TokenStream::new()
};
let oneof_mod = if !oneof.is_empty() {
quote! {
pub mod oneof {
#[allow(unused_imports)]
use super::*;
#(#oneof)*
}
}
} else {
TokenStream::new()
};
let ext_mod = if !ext.is_empty() {
quote! {
pub mod ext {
#[allow(unused_imports)]
use super::*;
#(#ext)*
}
}
} else {
TokenStream::new()
};
let register_fn = if ctx.config.emit_register_fn && !reg.is_empty() {
let gates = ctx.config.feature_gates();
let json_regs = reg
.json_any
.iter()
.map(|p| {
feature_gates::cfg_block(quote! { reg.register_json_any(super::#p); }, gates.json)
})
.chain(reg.json_ext.iter().map(|p| {
feature_gates::cfg_block(quote! { reg.register_json_ext(super::#p); }, gates.json)
}));
let text_regs = reg
.text_any
.iter()
.map(|p| {
feature_gates::cfg_block(quote! { reg.register_text_any(super::#p); }, gates.text)
})
.chain(reg.text_ext.iter().map(|p| {
feature_gates::cfg_block(quote! { reg.register_text_ext(super::#p); }, gates.text)
}));
let allow_unused = if ctx.config.gate_impls_on_crate_features {
quote! { #[allow(unused_variables)] }
} else {
quote! {}
};
feature_gates::cfg_block_any(
quote! {
#allow_unused
pub fn register_types(reg: &mut ::buffa::type_registry::TypeRegistry) {
#(#json_regs)*
#(#text_regs)*
}
},
&gates.json_or_text(),
)
} else {
TokenStream::new()
};
let (reflect_mod, reflect_reexport) = if ctx.config.generate_reflection {
let gate = ctx.config.feature_gates().reflect;
(
feature_gates::cfg_block(reflect::reflect_pool_module(fds_bytes), gate),
feature_gates::cfg_block(reflect::pool_accessor_reexport("e! { __buffa }), gate),
)
} else {
(TokenStream::new(), TokenStream::new())
};
let sentinel = make_field_ident(context::SENTINEL_MOD);
let buffa_mod = if view_mod.is_empty()
&& lazy_view_mod.is_empty()
&& oneof_mod.is_empty()
&& ext_mod.is_empty()
&& register_fn.is_empty()
&& reflect_mod.is_empty()
&& custom_elem_impls.is_empty()
{
TokenStream::new()
} else {
let allow = allow_lints_attr();
quote! {
#allow
pub mod #sentinel {
#[allow(unused_imports)]
use super::*;
#view_mod
#lazy_view_mod
#oneof_mod
#ext_mod
#register_fn
#reflect_mod
#custom_elem_impls
}
}
};
let use_block = ctx.imports_use_block();
let tokens = quote! {
#use_block
#(#owned)*
#buffa_mod
#reflect_reexport
#root_reexports
};
format_tokens(tokens, "")
}
fn format_tokens(tokens: TokenStream, source: &str) -> Result<String, CodeGenError> {
let syntax_tree =
syn::parse2::<syn::File>(tokens).map_err(|e| CodeGenError::InvalidSyntax(e.to_string()))?;
let formatted = prettyplease::unparse(&syntax_tree);
let source_line = if source.is_empty() {
String::new()
} else {
format!("// source: {source}\n")
};
Ok(format!(
"// @generated by buffa-codegen. DO NOT EDIT.\n{source_line}\n{formatted}"
))
}
pub fn package_to_mod_filename(package: &str) -> String {
if package.is_empty() {
format!("{}.mod.rs", context::SENTINEL_MOD)
} else {
format!("{package}.mod.rs")
}
}
pub fn package_to_filename(package: &str) -> String {
if package.is_empty() {
format!("{}.rs", context::SENTINEL_MOD)
} else {
format!("{package}.rs")
}
}
pub fn proto_path_to_stem(proto_path: &str) -> String {
let without_ext = proto_path.strip_suffix(".proto").unwrap_or(proto_path);
without_ext.replace('/', ".")
}
pub fn apply_companions(files: &mut Vec<GeneratedFile>, companions: Vec<GeneratedFile>) {
for comp in &companions {
debug_assert!(
!comp.name.contains(['"', '\\', '/', '\n']),
"companion file name {:?} contains a character that would break \
the generated include!() literal or its bare-sibling resolution",
comp.name
);
if let Some(pkg_mod) = files
.iter_mut()
.find(|f| f.kind == GeneratedFileKind::PackageMod && f.package == comp.package)
{
pkg_mod
.content
.push_str(&format!("include!(\"{}\");\n", comp.name));
}
}
files.extend(companions);
}
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum CodeGenError {
#[error("missing required descriptor field: {0}")]
MissingField(&'static str),
#[error("invalid Rust type path: '{0}'")]
InvalidTypePath(String),
#[error("box_type template must contain a `*` placeholder for the message type: '{0}'")]
MissingWildcard(String),
#[error("repeated_type template must contain a `*` element placeholder: '{0}'")]
MissingListPlaceholder(String),
#[error("generated code failed to parse as Rust: {0}")]
InvalidSyntax(String),
#[error("file_to_generate '{0}' not found in descriptor set")]
FileNotFound(String),
#[error("codegen error: {0}")]
Other(String),
#[error(
"reserved field name '{field_name}' in message '{message_name}': \
proto field names starting with '__buffa_' conflict with buffa's \
internal fields"
)]
ReservedFieldName {
message_name: String,
field_name: String,
},
#[error(
"module name conflict in '{scope}': messages '{name_a}' and '{name_b}' \
both produce module '{module_name}'"
)]
ModuleNameConflict {
scope: String,
name_a: String,
name_b: String,
module_name: String,
},
#[error(
"reserved name '{name}' at {location}: this name is reserved for \
buffa's generated ancillary types (views, oneof enums, \
extensions). Rename the proto element."
)]
ReservedModuleName { name: String, location: String },
#[error(
"message '{message_name}' uses `option message_set_wire_format = true` \
but CodeGenConfig::allow_message_set is false; MessageSet is a legacy \
wire format — set allow_message_set(true) if this is intentional"
)]
MessageSetNotSupported { message_name: String },
#[error(
"invalid custom attribute for path '{path}': '{attribute}' is not a valid \
Rust attribute ({detail})"
)]
InvalidCustomAttribute {
path: String,
attribute: String,
detail: String,
},
#[error(
"invalid type_name_prefix '{prefix}': must be empty or PascalCase \
(start with an ASCII uppercase letter, followed by ASCII letters \
and digits only)"
)]
InvalidTypeNamePrefix { prefix: String },
}
#[cfg(test)]
mod tests;