#![forbid(unsafe_code)]
use prost_types::{
DescriptorProto, EnumDescriptorProto, FieldDescriptorProto, FileDescriptorProto,
FileDescriptorSet, SourceCodeInfo,
};
pub use crate::options::{CodegenError, CodegenOptions};
use crate::type_registry::TypeRegistry;
pub fn emit_file_descriptor_set(fds: &FileDescriptorSet) -> Result<String, CodegenError> {
emit_file_descriptor_set_with_options(fds, &CodegenOptions::default())
}
pub fn emit_file_descriptor_set_with_options(
fds: &FileDescriptorSet,
options: &CodegenOptions,
) -> Result<String, CodegenError> {
let registry = TypeRegistry::build(fds, options.package_namespacing);
let mut out = String::new();
out.push_str("// Generated by oxiproto-codegen. Do not edit.\n\n");
if options.emit_json && !options.package_namespacing {
out.push_str(&crate::json_impl::emit_json_file_prelude());
}
let has_maps = fds.file.iter().any(file_has_map_fields);
if has_maps {
if options.use_btree_map_effective() {
out.push_str("use std::collections::BTreeMap;\n\n");
} else {
out.push_str("use std::collections::HashMap;\n\n");
}
}
if options.package_namespacing {
let mut pkg_map: std::collections::BTreeMap<String, String> =
std::collections::BTreeMap::new();
for file in &fds.file {
let package = file.package.as_deref().unwrap_or("").to_string();
let mut file_out = String::new();
emit_file_content(&mut file_out, file, options, &package, ®istry)?;
if !file_out.trim().is_empty() {
pkg_map.entry(package).or_default().push_str(&file_out);
}
}
for (package, content) in &pkg_map {
if package.is_empty() {
if options.emit_json {
out.push_str(&crate::json_impl::emit_json_file_prelude());
}
out.push_str(content);
} else {
emit_package_modules(&mut out, package, content, options);
}
}
} else {
for file in &fds.file {
let package = file.package.as_deref().unwrap_or("").to_string();
emit_file_content(&mut out, file, options, &package, ®istry)?;
}
}
Ok(out)
}
fn emit_package_modules(out: &mut String, package: &str, content: &str, options: &CodegenOptions) {
let parts: Vec<&str> = package.split('.').collect();
for (depth, part) in parts.iter().enumerate() {
let indent = " ".repeat(depth);
out.push_str(&format!("{indent}pub mod {part} {{\n"));
}
let indent = " ".repeat(parts.len());
if options.emit_json {
for line in crate::json_impl::emit_json_file_prelude().lines() {
if line.trim().is_empty() {
out.push('\n');
} else {
out.push_str(&format!("{indent}{line}\n"));
}
}
}
for line in content.lines() {
if line.trim().is_empty() {
out.push('\n');
} else {
out.push_str(&format!("{indent}{line}\n"));
}
}
for depth in (0..parts.len()).rev() {
let indent = " ".repeat(depth);
out.push_str(&format!("{indent}}}\n"));
}
out.push('\n');
}
fn file_has_map_fields(file: &FileDescriptorProto) -> bool {
file.message_type.iter().any(message_has_map_fields)
}
fn message_has_map_fields(msg: &DescriptorProto) -> bool {
msg.nested_type
.iter()
.any(|n| n.options.as_ref().is_some_and(|o| o.map_entry()))
|| msg.nested_type.iter().any(message_has_map_fields)
}
fn emit_file_content(
out: &mut String,
file: &FileDescriptorProto,
options: &CodegenOptions,
file_package: &str,
registry: &TypeRegistry,
) -> Result<(), CodegenError> {
let source_info = file.source_code_info.as_ref();
for (idx, msg) in file.message_type.iter().enumerate() {
let path = vec![4, idx as i32];
emit_message(
out,
msg,
&[],
options,
source_info,
&path,
file_package,
registry,
)?;
}
for (idx, en) in file.enum_type.iter().enumerate() {
let path = vec![5, idx as i32];
emit_enum(out, en, options, source_info, &path)?;
}
if options.emit_services {
for (idx, svc) in file.service.iter().enumerate() {
let path = vec![6, idx as i32];
emit_service(out, svc, options, source_info, &path)?;
}
}
Ok(())
}
fn emit_file(
out: &mut String,
file: &FileDescriptorProto,
options: &CodegenOptions,
registry: &TypeRegistry,
) -> Result<(), CodegenError> {
let file_package = file.package.as_deref().unwrap_or("");
emit_file_content(out, file, options, file_package, registry)
}
#[allow(dead_code)]
pub(crate) fn emit_file_compat(
out: &mut String,
file: &FileDescriptorProto,
options: &CodegenOptions,
) -> Result<(), CodegenError> {
let fds = prost_types::FileDescriptorSet {
file: vec![file.clone()],
};
let registry = TypeRegistry::build(&fds, options.package_namespacing);
emit_file(out, file, options, ®istry)
}
fn fully_qualified_type_name(name: &str, file_package: &str) -> String {
if file_package.is_empty() {
name.to_string()
} else {
format!("{file_package}.{name}")
}
}
fn reserved_numbers(msg: &DescriptorProto) -> std::collections::HashSet<i32> {
let mut set = std::collections::HashSet::new();
for range in &msg.reserved_range {
let start = range.start.unwrap_or(0);
let end = range.end.unwrap_or(0);
for n in start..end {
set.insert(n);
}
}
set
}
fn reserved_names(msg: &DescriptorProto) -> std::collections::HashSet<&str> {
msg.reserved_name.iter().map(|s| s.as_str()).collect()
}
#[allow(clippy::too_many_arguments)]
fn emit_message(
out: &mut String,
msg: &DescriptorProto,
name_prefix: &[&str],
options: &CodegenOptions,
source_info: Option<&SourceCodeInfo>,
path: &[i32],
file_package: &str,
registry: &TypeRegistry,
) -> Result<(), CodegenError> {
let name = msg
.name
.as_deref()
.ok_or_else(|| CodegenError::InvalidDescriptor("message missing name".into()))?;
if msg.options.as_ref().is_some_and(|o| o.map_entry()) {
return Ok(());
}
let full_name = if name_prefix.is_empty() {
name.to_string()
} else {
format!("{}_{}", name_prefix.join("_"), name)
};
let fq_name = fully_qualified_type_name(&full_name, file_package);
let res_nums = reserved_numbers(msg);
let res_names = reserved_names(msg);
let oneofs = collect_oneofs(msg)?;
let oneof_field_indices: Vec<Option<usize>> = msg
.field
.iter()
.map(|f| f.oneof_index.map(|i| i as usize))
.collect();
let map_entries = collect_map_entries(msg, file_package, registry);
let map_field_names: std::collections::HashSet<String> = map_entries.keys().cloned().collect();
if let Some(attrs) = options.type_attributes.get(&fq_name) {
for attr in attrs {
out.push_str(attr);
out.push('\n');
}
}
if options.generate_docs {
emit_leading_comments(out, source_info, path, 0);
}
let is_deprecated =
options.generate_deprecated && msg.options.as_ref().is_some_and(|o| o.deprecated());
if is_deprecated {
out.push_str("#[deprecated]\n");
}
out.push_str("#[derive(Debug, Clone, PartialEq, Default)]\n");
out.push_str(&format!("pub struct {full_name} {{\n"));
let mut emitted_oneofs = vec![false; oneofs.len()];
for (field_idx, field) in msg.field.iter().enumerate() {
let fname = field
.name
.as_deref()
.ok_or_else(|| CodegenError::InvalidDescriptor("field missing name".into()))?;
let field_number = field.number.unwrap_or(0);
if res_nums.contains(&field_number) || res_names.contains(fname) {
let comment_target = if res_names.contains(fname) {
fname.to_string()
} else {
format!("{field_number}")
};
out.push_str(&format!(" // reserved field {comment_target}\n"));
continue;
}
let fq_field_key = format!("{fq_name}.{fname}");
if let Some(fattrs) = options.field_attributes.get(&fq_field_key) {
for attr in fattrs {
out.push_str(" ");
out.push_str(attr);
out.push('\n');
}
}
if options.generate_docs {
let mut field_path = path.to_vec();
field_path.push(2); field_path.push(field_idx as i32);
emit_leading_comments(out, source_info, &field_path, 4);
}
if options.generate_deprecated && field.options.as_ref().is_some_and(|o| o.deprecated()) {
out.push_str(" #[deprecated]\n");
}
if let Some(oneof_idx) = oneof_field_indices[field_idx] {
if !emitted_oneofs[oneof_idx] {
emitted_oneofs[oneof_idx] = true;
let oneof = &oneofs[oneof_idx];
let oneof_type = format!("{full_name}_{}", to_pascal_case(&oneof.name));
out.push_str(&format!(" pub {}: Option<{oneof_type}>,\n", oneof.name));
}
continue;
}
if let Some(map_info) = map_entries.get(fname) {
let map_type = if options.use_btree_map_effective() {
"BTreeMap"
} else {
"HashMap"
};
out.push_str(&format!(
" pub {fname}: {map_type}<{}, {}>,\n",
map_info.key_type, map_info.value_type
));
continue;
}
let ftype = field_type_str_with_wkt(field, &full_name, file_package, registry)?;
out.push_str(&format!(" pub {fname}: {ftype},\n"));
}
if options.emit_oxi_message_impl {
out.push_str(" #[doc(hidden)]\n");
out.push_str(" pub _unknown: ::oxiproto_core::wire::UnknownFields,\n");
}
out.push_str("}\n\n");
for (oneof_idx, oneof) in oneofs.iter().enumerate() {
let oneof_type = format!("{full_name}_{}", to_pascal_case(&oneof.name));
emit_oneof_enum(
out,
&oneof_type,
&msg.field,
oneof_idx,
&full_name,
options,
file_package,
registry,
)?;
}
if options.emit_oxi_message_impl {
let impl_code = crate::message_impl::emit_oxi_message_impl(
msg,
&full_name,
file_package,
&map_field_names,
)?;
out.push_str(&impl_code);
let name_code = crate::message_impl::emit_oxi_name_impl(msg, &full_name, file_package);
out.push_str(&name_code);
}
if options.emit_json {
let json_code = crate::json_impl::emit_json_impls(
msg,
&full_name,
file_package,
&map_field_names,
registry,
)?;
out.push_str(&json_code);
}
if options.emit_builder {
let builder_code = crate::builder_impl::emit_builder_for_message(
msg,
&full_name,
options,
file_package,
registry,
)?;
out.push_str(&builder_code);
}
if options.emit_text_format {
let text_code = crate::text_impl::emit_text_format_impl(
msg,
&full_name,
options,
file_package,
registry,
)?;
out.push_str(&text_code);
}
let prefix: Vec<&str> = name_prefix
.iter()
.copied()
.chain(std::iter::once(name))
.collect();
for (idx, nested) in msg.nested_type.iter().enumerate() {
let mut nested_path = path.to_vec();
nested_path.push(3); nested_path.push(idx as i32);
emit_message(
out,
nested,
&prefix,
options,
source_info,
&nested_path,
file_package,
registry,
)?;
}
for (idx, en) in msg.enum_type.iter().enumerate() {
let mut enum_path = path.to_vec();
enum_path.push(4); enum_path.push(idx as i32);
emit_enum(out, en, options, source_info, &enum_path)?;
}
Ok(())
}
pub(crate) struct MapFieldInfo {
pub(crate) key_type: String,
pub(crate) value_type: String,
}
pub(crate) fn collect_map_entries(
msg: &DescriptorProto,
file_package: &str,
registry: &TypeRegistry,
) -> std::collections::BTreeMap<String, MapFieldInfo> {
let mut result = std::collections::BTreeMap::new();
for nested in &msg.nested_type {
let is_map_entry = nested.options.as_ref().is_some_and(|o| o.map_entry());
if !is_map_entry {
continue;
}
let entry_name = nested.name.as_deref().unwrap_or("");
for field in &msg.field {
let type_name = field.type_name.as_deref().unwrap_or("");
let type_last = type_name.split('.').next_back().unwrap_or("");
if type_last != entry_name {
continue;
}
let field_name = field.name.as_deref().unwrap_or("");
if field_name.is_empty() {
continue;
}
let key_type = nested
.field
.iter()
.find(|f| f.name.as_deref() == Some("key"))
.map(|f| scalar_type_string(f, file_package, registry))
.unwrap_or_else(|| "String".to_string());
let value_type = nested
.field
.iter()
.find(|f| f.name.as_deref() == Some("value"))
.map(|f| scalar_type_string(f, file_package, registry))
.unwrap_or_else(|| "String".to_string());
result.insert(
field_name.to_string(),
MapFieldInfo {
key_type,
value_type,
},
);
}
}
result
}
fn scalar_type_string(
field: &FieldDescriptorProto,
file_package: &str,
registry: &TypeRegistry,
) -> String {
use prost_types::field_descriptor_proto::Type;
let ftype = field.r#type.unwrap_or(Type::String as i32);
if ftype == Type::Message as i32 || ftype == Type::Enum as i32 {
let raw_type_name = field.type_name.as_deref().unwrap_or("");
return registry.resolve(file_package, raw_type_name);
}
match ftype {
t if t == Type::Int32 as i32 || t == Type::Sint32 as i32 || t == Type::Sfixed32 as i32 => {
"i32".to_string()
}
t if t == Type::Int64 as i32 || t == Type::Sint64 as i32 || t == Type::Sfixed64 as i32 => {
"i64".to_string()
}
t if t == Type::Uint32 as i32 || t == Type::Fixed32 as i32 => "u32".to_string(),
t if t == Type::Uint64 as i32 || t == Type::Fixed64 as i32 => "u64".to_string(),
t if t == Type::Float as i32 => "f32".to_string(),
t if t == Type::Double as i32 => "f64".to_string(),
t if t == Type::Bool as i32 => "bool".to_string(),
t if t == Type::String as i32 => "String".to_string(),
t if t == Type::Bytes as i32 => "Vec<u8>".to_string(),
_ => "String".to_string(),
}
}
struct OneofInfo {
name: String,
}
fn collect_oneofs(msg: &DescriptorProto) -> Result<Vec<OneofInfo>, CodegenError> {
let mut oneofs = Vec::new();
for oneof in &msg.oneof_decl {
let name = oneof
.name
.as_deref()
.ok_or_else(|| CodegenError::InvalidDescriptor("oneof missing name".into()))?;
oneofs.push(OneofInfo {
name: name.to_string(),
});
}
Ok(oneofs)
}
#[allow(clippy::too_many_arguments)]
fn emit_oneof_enum(
out: &mut String,
enum_name: &str,
fields: &[FieldDescriptorProto],
oneof_index: usize,
parent_struct: &str,
_options: &CodegenOptions,
file_package: &str,
registry: &TypeRegistry,
) -> Result<(), CodegenError> {
let oneof_fields: Vec<&FieldDescriptorProto> = fields
.iter()
.filter(|f| f.oneof_index == Some(oneof_index as i32))
.collect();
if oneof_fields.is_empty() {
return Ok(());
}
out.push_str("#[allow(clippy::all, non_camel_case_types, clippy::enum_variant_names)]\n");
out.push_str("#[derive(Debug, Clone, PartialEq)]\n");
out.push_str(&format!("pub enum {enum_name} {{\n"));
for field in &oneof_fields {
let fname = field
.name
.as_deref()
.ok_or_else(|| CodegenError::InvalidDescriptor("oneof field missing name".into()))?;
let variant_name = to_pascal_case(fname);
let ftype = oneof_field_type_str(field, parent_struct, file_package, registry)?;
out.push_str(&format!(" {variant_name}({ftype}),\n"));
}
out.push_str("}\n\n");
Ok(())
}
fn oneof_field_type_str(
field: &FieldDescriptorProto,
_parent_struct: &str,
file_package: &str,
registry: &TypeRegistry,
) -> Result<String, CodegenError> {
use prost_types::field_descriptor_proto::Type;
let ftype = field.r#type.unwrap_or(Type::String as i32);
let raw_type_name = field.type_name.as_deref().unwrap_or("");
if ftype == Type::Message as i32 {
let n = registry.resolve(file_package, raw_type_name);
return Ok(format!("Box<{n}>"));
}
Ok(scalar_type_string(field, file_package, registry))
}
fn emit_enum(
out: &mut String,
en: &EnumDescriptorProto,
options: &CodegenOptions,
source_info: Option<&SourceCodeInfo>,
path: &[i32],
) -> Result<(), CodegenError> {
let name = en
.name
.as_deref()
.ok_or_else(|| CodegenError::InvalidDescriptor("enum missing name".into()))?;
if options.generate_docs {
emit_leading_comments(out, source_info, path, 0);
}
let is_deprecated =
options.generate_deprecated && en.options.as_ref().is_some_and(|o| o.deprecated());
if is_deprecated {
out.push_str("#[deprecated]\n");
}
out.push_str("#[allow(clippy::enum_variant_names)]\n");
out.push_str("#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\n");
out.push_str("#[repr(i32)]\n");
out.push_str(&format!("pub enum {name} {{\n"));
for (val_idx, val) in en.value.iter().enumerate() {
let vname = val
.name
.as_deref()
.ok_or_else(|| CodegenError::InvalidDescriptor("enum value missing name".into()))?;
let num = val.number.unwrap_or(0);
let variant = to_pascal_case_enum(vname, name);
if options.generate_docs {
let mut val_path = path.to_vec();
val_path.push(2);
val_path.push(val_idx as i32);
emit_leading_comments(out, source_info, &val_path, 4);
}
out.push_str(&format!(" {variant} = {num},\n"));
}
out.push_str("}\n\n");
if let Some(first_val) = en.value.first() {
let first_name = first_val.name.as_deref().unwrap_or("UNKNOWN");
let first_variant = to_pascal_case_enum(first_name, name);
out.push_str("#[allow(clippy::derivable_impls)]\n");
out.push_str(&format!("impl Default for {name} {{\n"));
out.push_str(" fn default() -> Self {\n");
out.push_str(&format!(" {name}::{first_variant}\n"));
out.push_str(" }\n");
out.push_str("}\n\n");
}
out.push_str(&format!("impl {name} {{\n"));
out.push_str(" /// Convert from an i32 value, returning `None` for unknown values.\n");
out.push_str(" pub fn from_i32(value: i32) -> Option<Self> {\n");
out.push_str(" match value {\n");
for val in &en.value {
let vname = val.name.as_deref().unwrap_or("UNKNOWN");
let num = val.number.unwrap_or(0);
let variant = to_pascal_case_enum(vname, name);
out.push_str(&format!(" {num} => Some({name}::{variant}),\n"));
}
out.push_str(" _ => None,\n");
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str("}\n\n");
if options.emit_json {
out.push_str(&crate::json_impl::emit_enum_json_impl(en, name)?);
}
Ok(())
}
fn emit_service(
out: &mut String,
svc: &prost_types::ServiceDescriptorProto,
options: &CodegenOptions,
source_info: Option<&SourceCodeInfo>,
path: &[i32],
) -> Result<(), CodegenError> {
let name = svc
.name
.as_deref()
.ok_or_else(|| CodegenError::InvalidDescriptor("service missing name".into()))?;
if options.generate_docs {
emit_leading_comments(out, source_info, path, 0);
}
let is_deprecated =
options.generate_deprecated && svc.options.as_ref().is_some_and(|o| o.deprecated());
if is_deprecated {
out.push_str("#[deprecated]\n");
}
out.push_str(&format!("pub trait {name} {{\n"));
for (method_idx, method) in svc.method.iter().enumerate() {
let method_name = method
.name
.as_deref()
.ok_or_else(|| CodegenError::InvalidDescriptor("method missing name".into()))?;
let rust_method_name = to_snake_case(method_name);
if options.generate_docs {
let mut method_path = path.to_vec();
method_path.push(2);
method_path.push(method_idx as i32);
emit_leading_comments(out, source_info, &method_path, 4);
}
let input_type = method
.input_type
.as_deref()
.map(|t| last_component(t.trim_start_matches('.')))
.unwrap_or_else(|| "()".to_string());
let output_type = method
.output_type
.as_deref()
.map(|t| last_component(t.trim_start_matches('.')))
.unwrap_or_else(|| "()".to_string());
let client_streaming = method.client_streaming.unwrap_or(false);
let server_streaming = method.server_streaming.unwrap_or(false);
let (req_type, resp_type) = match (client_streaming, server_streaming) {
(false, false) => (input_type.clone(), output_type.clone()),
(false, true) => (input_type.clone(), format!("Vec<{output_type}>")),
(true, false) => (format!("Vec<{input_type}>"), output_type.clone()),
(true, true) => (format!("Vec<{input_type}>"), format!("Vec<{output_type}>")),
};
out.push_str(&format!(
" fn {rust_method_name}(&self, request: {req_type}) -> Result<{resp_type}, Box<dyn std::error::Error>>;\n"
));
}
out.push_str("}\n\n");
Ok(())
}
fn emit_leading_comments(
out: &mut String,
source_info: Option<&SourceCodeInfo>,
path: &[i32],
indent: usize,
) {
let Some(sci) = source_info else {
return;
};
let pad = " ".repeat(indent);
for loc in &sci.location {
if loc.path == path {
if let Some(leading) = &loc.leading_comments {
for line in leading.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
out.push_str(&format!("{pad}///\n"));
} else {
out.push_str(&format!("{pad}/// {trimmed}\n"));
}
}
}
}
}
}
pub(crate) fn field_type_str_with_wkt(
field: &FieldDescriptorProto,
struct_name: &str,
file_package: &str,
registry: &TypeRegistry,
) -> Result<String, CodegenError> {
use prost_types::field_descriptor_proto::{Label, Type};
let label = field.label.unwrap_or(Label::Optional as i32);
let ftype = field.r#type.unwrap_or(Type::String as i32);
let repeated = label == Label::Repeated as i32;
let raw_type_name = field.type_name.as_deref().unwrap_or("");
if ftype == Type::Message as i32 && !raw_type_name.is_empty() {
let normalized = raw_type_name.trim_start_matches('.');
let lookup_with_dot = format!(".{normalized}");
if let Some(wkt) = crate::wkt_map::wkt_rust_type(&lookup_with_dot) {
return if repeated {
Ok(format!("Vec<{wkt}>"))
} else {
Ok(wkt.to_string())
};
}
}
field_type_str(field, struct_name, file_package, registry)
}
fn field_type_str(
field: &FieldDescriptorProto,
_struct_name: &str,
file_package: &str,
registry: &TypeRegistry,
) -> Result<String, CodegenError> {
use prost_types::field_descriptor_proto::{Label, Type};
let label = field.label.unwrap_or(Label::Optional as i32);
let ftype = field.r#type.unwrap_or(Type::String as i32);
let repeated = label == Label::Repeated as i32;
let raw_type_name = field.type_name.as_deref().unwrap_or("");
if ftype == Type::Message as i32 {
let n = registry.resolve(file_package, raw_type_name);
return if repeated {
Ok(format!("Vec<{n}>"))
} else {
Ok(format!("Option<Box<{n}>>"))
};
}
let base: String = scalar_type_string(field, file_package, registry);
if repeated {
Ok(format!("Vec<{base}>"))
} else {
Ok(base)
}
}
fn last_component(s: &str) -> String {
s.split('.').next_back().unwrap_or(s).to_string()
}
pub(crate) fn to_pascal_case(s: &str) -> String {
s.split('_')
.map(|part| {
let mut chars = part.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().to_string() + &chars.as_str().to_lowercase(),
}
})
.collect()
}
pub(crate) fn to_pascal_case_pub(s: &str) -> String {
to_pascal_case(s)
}
fn to_snake_case(s: &str) -> String {
let mut result = String::with_capacity(s.len() + 4);
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() && i > 0 {
result.push('_');
}
result.push(c.to_ascii_lowercase());
}
result
}
fn to_pascal_case_enum(value_name: &str, _enum_name: &str) -> String {
to_pascal_case(value_name)
}
#[derive(Debug, Clone, Default)]
pub struct ModuleTree {
pub name: String,
pub items: Vec<String>,
pub children: Vec<ModuleTree>,
}
impl ModuleTree {
fn get_or_insert_child(&mut self, name: &str) -> &mut ModuleTree {
let pos = match self.children.iter().position(|c| c.name == name) {
Some(p) => p,
None => {
self.children.push(ModuleTree {
name: name.to_string(),
..Default::default()
});
self.children.len() - 1
}
};
&mut self.children[pos]
}
fn navigate_to(&mut self, path: &[&str]) -> &mut ModuleTree {
let mut node = self;
for &seg in path {
node = node.get_or_insert_child(seg);
}
node
}
pub fn render(&self) -> String {
let mut out = String::new();
for item in &self.items {
out.push_str(item);
out.push('\n');
}
for child in &self.children {
out.push_str(&format!("pub mod {} {{\n", child.name));
let inner = child.render();
for line in inner.lines() {
if line.is_empty() {
out.push('\n');
} else {
out.push_str(" ");
out.push_str(line);
out.push('\n');
}
}
out.push_str("}\n");
}
out
}
pub fn all_paths(&self) -> Vec<Vec<String>> {
let mut result = Vec::new();
self.collect_paths(&[], &mut result);
result
}
fn collect_paths(&self, prefix: &[String], out: &mut Vec<Vec<String>>) {
let path: Vec<String> = if self.name.is_empty() {
prefix.to_vec()
} else {
let mut p = prefix.to_vec();
p.push(self.name.clone());
p
};
out.push(path.clone());
for child in &self.children {
child.collect_paths(&path, out);
}
}
}
pub fn generate_module_tree(
fds: &FileDescriptorSet,
options: &CodegenOptions,
) -> Result<ModuleTree, CodegenError> {
use std::collections::BTreeMap;
let registry = TypeRegistry::build(fds, true);
let mut pkg_map: BTreeMap<String, Vec<String>> = BTreeMap::new();
for file in &fds.file {
let pkg = file.package.as_deref().unwrap_or("").to_string();
let mut file_out = String::new();
emit_file_content(&mut file_out, file, options, &pkg, ®istry)?;
pkg_map.entry(pkg).or_default().push(file_out);
}
let mut root = ModuleTree::default();
for (pkg, contents) in pkg_map {
if pkg.is_empty() {
for c in contents {
root.items.push(c);
}
} else {
let segs: Vec<&str> = pkg.split('.').collect();
let node = root.navigate_to(&segs);
for c in contents {
node.items.push(c);
}
}
}
Ok(root)
}