#![forbid(unsafe_code)]
use clap::Args;
use std::path::{Path, PathBuf};
use crate::util::Verbosity;
#[derive(Args)]
pub struct GenArgs {
#[arg(required = true)]
pub protos: Vec<PathBuf>,
#[arg(short, long, default_value = ".")]
pub output: PathBuf,
#[arg(short = 'I', long)]
pub include: Vec<PathBuf>,
#[arg(long, help = "Print generated code to stdout instead of writing files")]
pub dry_run: bool,
#[arg(long, help = "Generate JSON serialization impls alongside messages")]
pub json: bool,
#[arg(
long,
action = clap::ArgAction::Set,
default_value_t = true,
help = "Generate gRPC service traits"
)]
pub grpc: bool,
#[arg(long, help = "Process directories recursively for *.proto files")]
pub recursive: bool,
#[arg(long, help = "Generate prost-compatible output with derive macros")]
pub prost_compat: bool,
}
pub fn run(args: GenArgs, verbosity: Verbosity) -> Result<(), Box<dyn std::error::Error>> {
let mut all_protos: Vec<PathBuf> = Vec::new();
for input in &args.protos {
let files = collect_proto_files(input, args.recursive)?;
if files.is_empty() {
return Err(format!("no .proto files found at: {}", input.display()).into());
}
all_protos.extend(files);
}
if all_protos.is_empty() {
return Err("no .proto files to process".into());
}
for proto in &all_protos {
if !proto.exists() {
return Err(format!("proto file not found: {}", proto.display()).into());
}
}
verbosity.verbose(&format!("Processing {} proto file(s)", all_protos.len()));
if !args.dry_run {
std::fs::create_dir_all(&args.output)?;
}
let fds = oxiproto_build::compile_to_fds(&all_protos, &args.include)?;
if args.prost_compat {
if args.dry_run {
return Err("--dry-run is not supported with --prost-compat".into());
}
std::fs::create_dir_all(&args.output)?;
let mut config = prost_build::Config::new();
config.out_dir(&args.output);
config
.compile_fds(fds)
.map_err(|e| format!("prost-compat codegen failed: {e}"))?;
verbosity.info(&format!(
"Generated (prost-compat) in {}",
args.output.display()
));
return Ok(());
}
let mut codegen_opts = oxiproto_codegen::CodegenOptions::new();
codegen_opts.emit_services = args.grpc;
codegen_opts.emit_json = args.json;
let rust_source = oxiproto_codegen::generate_with_options(&fds, &codegen_opts)?;
let out_filename = derive_output_filename(&all_protos[0])?;
if args.dry_run {
verbosity.verbose("Dry run: printing to stdout");
print!("{rust_source}");
} else {
let out_file = args.output.join(&out_filename);
std::fs::write(&out_file, &rust_source)?;
verbosity.info(&format!("Generated: {}", out_file.display()));
}
Ok(())
}
fn derive_output_filename(proto_path: &Path) -> Result<String, Box<dyn std::error::Error>> {
if let Ok(contents) = std::fs::read_to_string(proto_path) {
for line in contents.lines().take(20) {
let trimmed = line.trim();
if trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with('*') {
continue;
}
if trimmed.starts_with("package ") && trimmed.ends_with(';') {
let pkg = trimmed
.trim_start_matches("package ")
.trim_end_matches(';')
.trim();
if !pkg.is_empty() {
let name = pkg.replace('.', "_");
return Ok(format!("{name}.rs"));
}
}
}
}
let stem = proto_path
.file_stem()
.and_then(|s| s.to_str())
.filter(|s| !s.is_empty())
.ok_or_else(|| {
format!(
"cannot derive output filename from: {}",
proto_path.display()
)
})?;
Ok(format!("{stem}.rs"))
}
fn collect_proto_files(path: &Path, recursive: bool) -> std::io::Result<Vec<PathBuf>> {
if path.is_file() {
return Ok(vec![path.to_owned()]);
}
if !path.is_dir() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("path not found: {}", path.display()),
));
}
let mut result = Vec::new();
collect_proto_recursive(path, recursive, &mut result)?;
Ok(result)
}
fn collect_proto_recursive(
dir: &Path,
recursive: bool,
out: &mut Vec<PathBuf>,
) -> std::io::Result<()> {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str == ".git" || name_str == "target" {
continue;
}
if path.is_dir() && recursive {
collect_proto_recursive(&path, recursive, out)?;
} else if path.extension().map(|e| e == "proto").unwrap_or(false) {
out.push(path);
}
}
Ok(())
}