use std::io::{self, Read, Write};
use buffa::Message;
use buffa_codegen::generated::compiler::code_generator_response::File as CodeGeneratorResponseFile;
use buffa_codegen::generated::compiler::{CodeGeneratorRequest, CodeGeneratorResponse};
use buffa_codegen::generated::descriptor::Edition;
use buffa_codegen::CodeGenConfig;
const HELP: &str = "\
protoc-gen-buffa — protoc plugin for generating Rust code with buffa.
This binary speaks the protoc plugin protocol: it reads a serialized
CodeGeneratorRequest from stdin and writes a CodeGeneratorResponse to
stdout. It is not intended to be invoked directly. Use it via protoc
or buf (with this binary on PATH):
protoc --buffa_out=. my_service.proto
# buf.gen.yaml
plugins:
- local: protoc-gen-buffa
out: src/gen
To point protoc at a binary not on PATH, use
--plugin=protoc-gen-buffa=/abs/path/to/protoc-gen-buffa
For a generated mod.rs module tree, also configure
protoc-gen-buffa-packaging.
Options are passed as a comma-separated parameter string, e.g.
--buffa_opt=views=true,json=true,extern_path=.my.pkg=::my_crate
See <https://github.com/anthropics/buffa/blob/main/docs/guide.md> for
the full option list.";
fn main() {
if let Some(arg) = std::env::args().nth(1) {
match arg.as_str() {
"--version" | "-V" => {
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
return;
}
"--help" | "-h" => {
println!("{HELP}");
return;
}
other => {
eprintln!(
"{}: unrecognized argument {other:?}. This is a protoc \
plugin; run with --help for usage.",
env!("CARGO_PKG_NAME")
);
std::process::exit(2);
}
}
}
match run() {
Ok(()) => {}
Err(e) => {
let response = CodeGeneratorResponse {
error: Some(format!("{}", e)),
supported_features: Some(feature_flags()),
..Default::default()
};
write_response(&response).unwrap_or_else(|io_err| {
eprintln!(
"protoc-gen-buffa: failed to write error response: {}",
io_err
);
std::process::exit(1);
});
}
}
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut input = Vec::new();
io::stdin().read_to_end(&mut input)?;
let request = CodeGeneratorRequest::decode_from_slice(&input)
.map_err(|e| format!("failed to decode CodeGeneratorRequest: {}", e))?;
let config = parse_config(request.parameter.as_deref().unwrap_or(""))?;
let generated = buffa_codegen::generate(
&request.proto_file,
&request.file_to_generate,
&config.codegen,
)?;
let files: Vec<CodeGeneratorResponseFile> = generated
.into_iter()
.map(|g| CodeGeneratorResponseFile {
name: Some(g.name),
content: Some(g.content),
..Default::default()
})
.collect();
let response = CodeGeneratorResponse {
supported_features: Some(feature_flags()),
minimum_edition: Some(Edition::EDITION_PROTO2 as i32),
maximum_edition: Some(Edition::EDITION_2024 as i32),
file: files,
..Default::default()
};
write_response(&response)?;
Ok(())
}
fn write_response(response: &CodeGeneratorResponse) -> io::Result<()> {
let mut output = Vec::new();
response.encode(&mut output);
io::stdout().write_all(&output)?;
io::stdout().flush()?;
Ok(())
}
fn feature_flags() -> u64 {
const FEATURE_PROTO3_OPTIONAL: u64 = 1;
const FEATURE_SUPPORTS_EDITIONS: u64 = 2;
FEATURE_PROTO3_OPTIONAL | FEATURE_SUPPORTS_EDITIONS
}
struct PluginConfig {
codegen: CodeGenConfig,
}
fn parse_config(params: &str) -> Result<PluginConfig, String> {
let mut codegen = CodeGenConfig::default();
if params.is_empty() {
return Ok(PluginConfig { codegen });
}
for param in params.split(',') {
let param = param.trim();
if let Some((key, value)) = param.split_once('=') {
match key.trim() {
"views" => codegen.generate_views = value.trim() == "true",
"unknown_fields" => codegen.preserve_unknown_fields = value.trim() != "false",
"json" => codegen.generate_json = value.trim() == "true",
"text" => codegen.generate_text = value.trim() == "true",
"arbitrary" => codegen.generate_arbitrary = value.trim() == "true",
"gate_impls" => codegen.gate_impls_on_crate_features = value.trim() == "true",
"allow_message_set" => codegen.allow_message_set = value.trim() == "true",
"strict_utf8" | "strict_utf8_mapping" => {
codegen.strict_utf8_mapping = value.trim() == "true"
}
"register_types" => codegen.emit_register_fn = value.trim() != "false",
"with_setters" => codegen.generate_with_setters = value.trim() != "false",
"reflection" => {
let mode = if value.trim() == "true" {
buffa_codegen::ReflectMode::VTable
} else {
buffa_codegen::ReflectMode::Off
};
mode.apply(&mut codegen);
}
"reflect_mode" => match value.trim() {
"off" => buffa_codegen::ReflectMode::Off.apply(&mut codegen),
"bridge" => buffa_codegen::ReflectMode::Bridge.apply(&mut codegen),
"vtable" => buffa_codegen::ReflectMode::VTable.apply(&mut codegen),
other => {
eprintln!(
"protoc-gen-buffa: invalid reflect_mode '{}', \
expected off, bridge, or vtable",
other
);
}
},
"file_per_package" => codegen.file_per_package = value.trim() == "true",
"extern_path" => {
if let Some((proto, rust)) = value.split_once('=') {
let mut proto = proto.trim().to_string();
if !proto.starts_with('.') {
proto.insert(0, '.');
}
codegen.extern_paths.push((proto, rust.trim().to_string()));
} else {
eprintln!(
"protoc-gen-buffa: invalid extern_path format '{}', \
expected 'extern_path=.proto.pkg=::rust::path' \
(or a type FQN, 'extern_path=.proto.pkg.Type=::rust::path::Type')",
value
);
}
}
"mod_file" => {
return Err("the mod_file option was removed in 0.2; use \
protoc-gen-buffa-packaging instead. See CHANGELOG \
for migration."
.to_string());
}
other => {
eprintln!("protoc-gen-buffa: unknown parameter '{}'", other);
}
}
}
}
Ok(PluginConfig { codegen })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_params_returns_defaults() {
let config = parse_config("").unwrap();
let defaults = CodeGenConfig::default();
assert_eq!(config.codegen.generate_views, defaults.generate_views);
assert_eq!(
config.codegen.preserve_unknown_fields,
defaults.preserve_unknown_fields
);
assert_eq!(config.codegen.generate_json, defaults.generate_json);
assert!(config.codegen.extern_paths.is_empty());
}
#[test]
fn views_true() {
let config = parse_config("views=true").unwrap();
assert!(config.codegen.generate_views);
}
#[test]
fn views_false() {
let config = parse_config("views=false").unwrap();
assert!(!config.codegen.generate_views);
}
#[test]
fn json_true() {
let config = parse_config("json=true").unwrap();
assert!(config.codegen.generate_json);
}
#[test]
fn unknown_fields_false() {
let config = parse_config("unknown_fields=false").unwrap();
assert!(!config.codegen.preserve_unknown_fields);
}
#[test]
fn unknown_fields_true() {
let config = parse_config("unknown_fields=true").unwrap();
assert!(config.codegen.preserve_unknown_fields);
}
#[test]
fn file_per_package_true() {
let config = parse_config("file_per_package=true").unwrap();
assert!(config.codegen.file_per_package);
}
#[test]
fn file_per_package_default_is_false() {
let config = parse_config("").unwrap();
assert!(!config.codegen.file_per_package);
}
#[test]
fn extern_path_with_leading_dot() {
let config = parse_config("extern_path=.my.common=::common_protos").unwrap();
assert_eq!(config.codegen.extern_paths.len(), 1);
assert_eq!(config.codegen.extern_paths[0].0, ".my.common");
assert_eq!(config.codegen.extern_paths[0].1, "::common_protos");
}
#[test]
fn extern_path_without_leading_dot_is_normalized() {
let config = parse_config("extern_path=my.common=::common_protos").unwrap();
assert_eq!(config.codegen.extern_paths[0].0, ".my.common");
}
#[test]
fn multiple_params() {
let config = parse_config("views=true,json=true").unwrap();
assert!(config.codegen.generate_views);
assert!(config.codegen.generate_json);
}
#[test]
fn multiple_extern_paths() {
let config =
parse_config("extern_path=.my.a=::crate_a,extern_path=.my.b=::crate_b").unwrap();
assert_eq!(config.codegen.extern_paths.len(), 2);
assert_eq!(config.codegen.extern_paths[0].0, ".my.a");
assert_eq!(config.codegen.extern_paths[1].0, ".my.b");
}
#[test]
fn whitespace_is_trimmed() {
let config = parse_config(" views = true , json = true ").unwrap();
assert!(config.codegen.generate_views);
assert!(config.codegen.generate_json);
}
#[test]
fn unknown_param_is_ignored() {
let config = parse_config("unknown_key=value").unwrap();
let defaults = CodeGenConfig::default();
assert_eq!(config.codegen.generate_views, defaults.generate_views);
}
#[test]
fn invalid_extern_path_is_ignored() {
let config = parse_config("extern_path=no_equals_sign").unwrap();
assert!(config.codegen.extern_paths.is_empty());
}
#[test]
fn register_types_false() {
let config = parse_config("register_types=false").unwrap();
assert!(!config.codegen.emit_register_fn);
}
#[test]
fn register_types_true() {
let config = parse_config("register_types=true").unwrap();
assert!(config.codegen.emit_register_fn);
}
#[test]
fn register_types_default_is_true() {
let config = parse_config("").unwrap();
assert!(config.codegen.emit_register_fn);
}
#[test]
fn gate_impls_true() {
let config = parse_config("gate_impls=true").unwrap();
assert!(config.codegen.gate_impls_on_crate_features);
}
#[test]
fn gate_impls_default_is_false() {
let config = parse_config("").unwrap();
assert!(!config.codegen.gate_impls_on_crate_features);
}
#[test]
fn with_setters_false() {
let config = parse_config("with_setters=false").unwrap();
assert!(!config.codegen.generate_with_setters);
}
#[test]
fn with_setters_default_is_true() {
let config = parse_config("").unwrap();
assert!(config.codegen.generate_with_setters);
}
#[test]
fn mod_file_errors_with_migration_hint() {
let err = parse_config("mod_file=mod.rs").err().unwrap();
assert!(err.contains("protoc-gen-buffa-packaging"));
}
#[test]
fn text_true() {
let config = parse_config("text=true").unwrap();
assert!(config.codegen.generate_text);
}
#[test]
fn text_default_is_false() {
let config = parse_config("").unwrap();
assert!(!config.codegen.generate_text);
}
#[test]
fn allow_message_set_true() {
let config = parse_config("allow_message_set=true").unwrap();
assert!(config.codegen.allow_message_set);
}
#[test]
fn allow_message_set_default_is_false() {
let config = parse_config("").unwrap();
assert!(!config.codegen.allow_message_set);
}
}