quack-builder 0.8.20250511

Rust code generator from .proto files. Initially forked from `https://github.com/tafia/quick-protobuf`, which seems kinda abandoned... :/ Bumped to https://crates.io/crates/nom/8.0.0
Documentation
use clap::Parser;
use quack_builder::{ConfigBuilder, errors::Error, types::FileDescriptor};
use std::path::{Path, PathBuf};

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// Generated file name, defaults to INPUT with 'rs' extension, cannot be used with --output_directory
    #[arg(short, long)]
    output: Option<String>,

    /// Output directory of generated code, cannot be used with --output
    #[arg(short = 'd', long)]
    output_directory: Option<String>,

    /// Path to search for imported protobufs
    #[arg(short = 'I', long)]
    include: Option<String>,

    /// Omit generation of modules for each package when there is only one package
    #[arg(short, long)]
    single_mod: bool,

    /// Show enums and messages in this .proto file, including those imported. No code generated
    #[arg(short, long)]
    no_output: bool,

    /// The .proto files used to generate quick-protobuf code
    #[arg(required = true)]
    input: Vec<String>,

    /// Error out if recursive messages do not have optional fields
    #[arg(short, long)]
    error_cycle: bool,

    /// Do not add module comments and module attributes in generated file
    #[arg(short = 'H', long)]
    no_headers: bool,

    /// The comma separated values to add to #[derive(...)] for every struct
    #[arg(short = 'C', long)]
    custom_struct_derive: Option<String>,

    /// The value to use for the optional #[repr(...)] for every struct
    #[arg(short = 'R', long)]
    custom_repr: Option<String>,

    /// Don't use Cow for String and Byte types
    #[arg(short = 'D', long)]
    dont_use_cow: bool,

    /// Generate Owned structs when the proto stuct has a lifetime
    #[arg(long)]
    owned: bool,

    /// Generate no_std compliant code
    #[arg(long)]
    nostd: bool,

    /// Use the hashbrown crate as the HashMap implementation
    #[arg(long)]
    hashbrown: bool,

    /// Generate MessageInfo implementations
    #[arg(long)]
    gen_info: bool,

    /// Add deprecated fields and mark them as #[deprecated]
    #[arg(long)]
    add_deprecated_fields: bool,

    /// Generate getters for fields with custom default values
    #[arg(long)]
    generate_getters: bool,
}

fn run() -> Result<(), Error> {
    let cli = Cli::parse();

    // Validate file extensions
    if let Some(output) = &cli.output {
        validate_extension(output, "rs").map_err(|e| Error::OutputFile(e))?;
    }
    for input in &cli.input {
        validate_extension(input, "proto").map_err(|e| Error::InputFile(e))?;
    }

    let in_files: Vec<PathBuf> = cli.input.iter().map(PathBuf::from).collect();
    let include_paths = cli
        .include
        .map(|p| vec![PathBuf::from(p)])
        .unwrap_or_default();
    let out_file = cli.output.map(PathBuf::from);
    let out_dir = cli.output_directory.map(PathBuf::from);
    let custom_repr = cli.custom_repr;
    let custom_struct_derive: Vec<String> = cli
        .custom_struct_derive
        .unwrap_or_default()
        .split(',')
        .map(|s| s.to_string())
        .collect();

    let compiler = ConfigBuilder::new(
        &in_files,
        out_file.as_ref(),
        out_dir.as_ref(),
        &include_paths,
    )?
    .single_module(cli.single_mod)
    .no_output(cli.no_output)
    .error_cycle(cli.error_cycle)
    .headers(!cli.no_headers)
    .dont_use_cow(cli.dont_use_cow)
    .custom_struct_derive(custom_struct_derive)
    .nostd(cli.nostd)
    .hashbrown(cli.hashbrown)
    .custom_repr(custom_repr)
    .owned(cli.owned)
    .add_deprecated_fields(cli.add_deprecated_fields)
    .generate_getters(cli.generate_getters);

    FileDescriptor::run(&compiler.build())
}

fn validate_extension(
    path: &str,
    expected: &str,
) -> Result<(), String> {
    match Path::new(path).extension() {
        Some(x) if x == expected => Ok(()),
        Some(x) => Err(format!(
            "Expected path with extension '{}', not: '{}'",
            expected,
            x.to_string_lossy()
        )),
        None => Err(format!("Expected path with extension '{}'", expected)),
    }
}

fn main() {
    ::std::process::exit({
        if let Err(e) = run() {
            eprintln!("quack-builder fatal error {}", e);
            let mut e: &dyn std::error::Error = &e;
            while let Some(err) = e.source() {
                eprintln!("caused by: {}", err);
                e = err;
            }
            1
        } else {
            0
        }
    });
}