Skip to content

Instantly share code, notes, and snippets.

@aquarhead
Last active August 15, 2023 12:35
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aquarhead/69092f21347353981357909bea7765e4 to your computer and use it in GitHub Desktop.
Save aquarhead/69092f21347353981357909bea7765e4 to your computer and use it in GitHub Desktop.
Fully Automated Rust Code Generation for Large Protobuf/gRPC Repos
use std::{collections::HashSet, env, fmt::Write, fs, path::Path};
use walkdir::WalkDir;
type Res = Result<(), Box<dyn std::error::Error>>;
// replace:
// "ROOT DIR": root dir of proto files to generate
// "INCLUDE DIR": where all "package" specifier based on
// "REPO DIR": where to monitor change for build.rs rerun
fn main() -> Res {
let mut protos = vec![];
let mut pkgs = HashSet::new();
for entry in WalkDir::new("ROOT DIR")
.into_iter()
.map(|e| e.unwrap())
.filter(|e| {
e.path()
.extension()
.map_or(false, |ext| ext.to_str().unwrap() == "proto")
})
{
let path = entry.path();
protos.push(path.to_owned());
let content = fs::read_to_string(&path).unwrap();
let pkg = content
.lines()
.find(|line| line.starts_with("package "))
.unwrap()
// remove comment
.split("//")
.next()
.unwrap()
// extract package
.trim()
.trim_start_matches("package ")
.trim_end_matches(";");
pkgs.insert(pkg.to_string());
}
tonic_build::configure()
.build_server(false)
.compile(&protos, &[Path::new("INCLUDE DIR").into()])?;
write_protos_rs(pkgs)?;
println!("cargo:rerun-if-changed=REPO DIR");
Ok(())
}
fn write_protos_rs(pkgs: HashSet<String>) -> Res {
let ref mut protos_rs = String::new();
let mut packages: Vec<String> = pkgs.into_iter().collect();
packages.sort();
let mut path_stack: Vec<String> = vec![];
for pkg in packages {
// find common ancestor
let pop_to = pkg
.split(".")
.map(|seg| map_keyword(seg))
.enumerate()
.position(|(idx, pkg_seg)| {
path_stack
.get(idx)
.map_or(true, |stack_seg| stack_seg != &pkg_seg)
})
.unwrap_or(0);
// pop stack
while path_stack.len() > pop_to {
path_stack.pop();
writeln!(protos_rs, "}}")?;
}
// now push stack
for seg in pkg.split(".").skip(pop_to).map(|seg| map_keyword(seg)) {
writeln!(protos_rs, "pub mod {} {{", &seg)?;
path_stack.push(seg);
}
// write include_proto! inside module
writeln!(
protos_rs,
"tonic::include_proto!(\"{}\");",
path_stack.join(".")
)?;
}
// pop all stack
while path_stack.len() > 0 {
path_stack.pop();
writeln!(protos_rs, "}}").unwrap();
}
fs::write(format!("{}/protos.rs", env::var("OUT_DIR")?), protos_rs)?;
Ok(())
}
// This is copied from prost-build/src/ident.rs
fn map_keyword(kw: &str) -> String {
let mut ident = kw.to_string();
match ident.as_str() {
// 2015 strict keywords.
"as" | "break" | "const" | "continue" | "else" | "enum" | "false"
| "fn" | "for" | "if" | "impl" | "in" | "let" | "loop" | "match" | "mod" | "move" | "mut"
| "pub" | "ref" | "return" | "static" | "struct" | "trait" | "true"
| "type" | "unsafe" | "use" | "where" | "while"
// 2018 strict keywords.
| "dyn"
// 2015 reserved keywords.
| "abstract" | "become" | "box" | "do" | "final" | "macro" | "override" | "priv" | "typeof"
| "unsized" | "virtual" | "yield"
// 2018 reserved keywords.
| "async" | "await" | "try" => ident.insert_str(0, "r#"),
// the following keywords are not supported as raw identifiers and are therefore suffixed with an underscore.
"self" | "super" | "extern" | "crate" => ident += "_",
_ => (),
}
ident
}
@rctrj
Copy link

rctrj commented Sep 25, 2022

Hi, Thanks for this. This works great.

I would suggest running cargo clippy as there are some changes that Clippy suggests.
You can check this repo I created. It has the Clippy suggested changes.

@mhuang74
Copy link

@aquarhead Thank you for the blog article and this gist. I used your technique in my googleads-rs project.

@aquarhead
Copy link
Author

@mhuang74 glad it helps :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment