@@ -7,6 +7,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
crunch-file.workspace = true
|
||||
crunch-codegen.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
tracing.workspace = true
|
||||
|
@@ -1,11 +1,10 @@
|
||||
mod logging;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use logging::LogArg;
|
||||
use tracing::Level;
|
||||
|
||||
#[derive(Parser, Clone)]
|
||||
#[command(author, version, about, long_about = None, subcommand_required = true)]
|
||||
@@ -43,13 +42,30 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
match &cli.commands {
|
||||
Commands::Generate {} => {
|
||||
let config_file = config::get_file(&cli.global_args.crunch_file)
|
||||
let config = config::get_file(&cli.global_args.crunch_file)
|
||||
.await
|
||||
.map_err(|e| anyhow!("failed to load config: {}", e))?
|
||||
.get_config()
|
||||
.map_err(|e| anyhow!("invalid config: {}", e))?;
|
||||
|
||||
tracing::info!("generating crunch code")
|
||||
tracing::info!("generating crunch code");
|
||||
let codegen = crunch_codegen::Codegen::new();
|
||||
|
||||
if let Some(publish) = config.publish {
|
||||
for p in &publish {
|
||||
let mut rel_schema_path = PathBuf::from(&p.schema_path);
|
||||
let mut rel_output_path = PathBuf::from(&p.output_path);
|
||||
|
||||
if let Some(dir_path) = cli.global_args.crunch_file.parent() {
|
||||
rel_schema_path = dir_path.join(rel_schema_path);
|
||||
rel_output_path = dir_path.join(rel_output_path);
|
||||
}
|
||||
|
||||
codegen
|
||||
.generate_rust(&rel_schema_path, &rel_output_path)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -17,3 +17,9 @@ prost.workspace = true
|
||||
prost-types.workspace = true
|
||||
prost-build.workspace = true
|
||||
bytes.workspace = true
|
||||
tempfile.workspace = true
|
||||
genco.workspace = true
|
||||
walkdir.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions.workspace = true
|
||||
|
@@ -1,15 +1,284 @@
|
||||
use anyhow::anyhow;
|
||||
use genco::prelude::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
pub struct Codegen {}
|
||||
|
||||
impl Codegen {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
pub async fn generate_rust(&self, input_path: &Path, output_path: &Path) -> anyhow::Result<()> {
|
||||
let input_protos = self.discover_files(input_path, "proto")?;
|
||||
let (input_proto_paths, input_dir) = self.copy_protos(input_protos, input_path).await?;
|
||||
let (output_proto_paths, output_dir) = self
|
||||
.generate_rust_from_proto(input_proto_paths, output_path, input_dir.path())
|
||||
.await?;
|
||||
|
||||
self.copy_rs(output_proto_paths, output_path, output_dir.path())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn discover_files(&self, input_path: &Path, extension: &str) -> anyhow::Result<Vec<PathBuf>> {
|
||||
let mut proto_files = Vec::new();
|
||||
for entry in WalkDir::new(input_path) {
|
||||
let entry = entry?;
|
||||
|
||||
if let Some(extension) = entry.path().extension().and_then(|e| e.to_str()) {
|
||||
proto_files.push(entry.into_path());
|
||||
}
|
||||
}
|
||||
|
||||
if proto_files.is_empty() {
|
||||
anyhow::anyhow!(
|
||||
"failed to find any protobuf files in: {}",
|
||||
input_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(proto_files)
|
||||
}
|
||||
|
||||
async fn copy_protos(
|
||||
&self,
|
||||
input_protos: Vec<PathBuf>,
|
||||
root_path: &Path,
|
||||
) -> anyhow::Result<(Vec<PathBuf>, tempfile::TempDir)> {
|
||||
let in_tempdir = tempfile::TempDir::new()?;
|
||||
let in_tempdir_path = in_tempdir.path();
|
||||
|
||||
let mut input_proto_paths = Vec::new();
|
||||
for input_proto in &input_protos {
|
||||
let rel_proto_path = input_proto.strip_prefix(root_path)?;
|
||||
let in_proto_path = in_tempdir_path.join(rel_proto_path);
|
||||
if let Some(dir) = in_proto_path.parent() {
|
||||
if !dir.exists() {
|
||||
tokio::fs::create_dir_all(dir).await?;
|
||||
}
|
||||
}
|
||||
|
||||
tokio::fs::copy(input_proto, &in_proto_path).await?;
|
||||
input_proto_paths.push(in_proto_path);
|
||||
}
|
||||
|
||||
Ok((input_proto_paths, in_tempdir))
|
||||
}
|
||||
|
||||
async fn generate_rust_from_proto(
|
||||
&self,
|
||||
input_proto_paths: Vec<PathBuf>,
|
||||
output_path: &Path,
|
||||
in_root_path: &Path,
|
||||
) -> anyhow::Result<(Vec<PathBuf>, tempfile::TempDir)> {
|
||||
let out_tempdir = tempfile::TempDir::new()?;
|
||||
let out_tempdir_path = out_tempdir.path();
|
||||
let handle = tokio::task::spawn_blocking({
|
||||
let out_tempdir_path = out_tempdir_path.to_path_buf();
|
||||
let in_root_path = in_root_path.to_path_buf();
|
||||
move || {
|
||||
prost_build::Config::new()
|
||||
.out_dir(out_tempdir_path)
|
||||
.compile_protos(input_proto_paths.as_slice(), &[in_root_path])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
});
|
||||
|
||||
let result: anyhow::Result<()> = handle.await?;
|
||||
result?;
|
||||
|
||||
let mut output_paths = self.discover_files(&out_tempdir_path, "rs")?;
|
||||
|
||||
let mod_path = self
|
||||
.generate_mod_file(&out_tempdir_path, &output_paths)
|
||||
.await?;
|
||||
output_paths.push(mod_path);
|
||||
|
||||
Ok((output_paths, out_tempdir))
|
||||
}
|
||||
|
||||
async fn generate_mod_file(
|
||||
&self,
|
||||
output_tempdir_path: &Path,
|
||||
output_paths: &[PathBuf],
|
||||
) -> anyhow::Result<PathBuf> {
|
||||
let mod_path = output_tempdir_path.join("mod.rs");
|
||||
let mut mod_file = tokio::fs::File::create(&mod_path).await?;
|
||||
|
||||
let mut includes: Vec<genco::lang::rust::Tokens> = Vec::new();
|
||||
for generated_file in output_paths {
|
||||
if let Some(name) = generated_file.file_name() {
|
||||
let mod_name = generated_file
|
||||
.file_stem()
|
||||
.unwrap()
|
||||
.to_ascii_lowercase()
|
||||
.to_string_lossy()
|
||||
.replace(".", "_")
|
||||
.replace("-", "_");
|
||||
|
||||
let file_name = name.to_str().unwrap();
|
||||
|
||||
includes.push(genco::quote! {
|
||||
pub mod $(mod_name) {
|
||||
include!($(quoted(file_name)));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mod_tokens: genco::lang::rust::Tokens = genco::quote! {
|
||||
$(for tokens in includes join($['\n']) => $tokens)
|
||||
};
|
||||
let mod_contents = mod_tokens.to_file_string()?;
|
||||
mod_file.write_all(mod_contents.as_bytes()).await?;
|
||||
|
||||
Ok(mod_path)
|
||||
}
|
||||
|
||||
async fn copy_rs(
|
||||
&self,
|
||||
output_proto_paths: Vec<PathBuf>,
|
||||
output_path: &Path,
|
||||
root_path: &Path,
|
||||
) -> anyhow::Result<()> {
|
||||
for output_rs in &output_proto_paths {
|
||||
let rel_proto_path = output_rs.strip_prefix(root_path).map_err(|e| {
|
||||
anyhow!(
|
||||
"output: {} does not match root_path: {}",
|
||||
output_rs.display(),
|
||||
root_path.display()
|
||||
)
|
||||
})?;
|
||||
let in_proto_path = output_path.join(rel_proto_path);
|
||||
if let Some(dir) = in_proto_path.parent() {
|
||||
if !dir.exists() {
|
||||
tokio::fs::create_dir_all(dir).await?;
|
||||
}
|
||||
}
|
||||
|
||||
tokio::fs::copy(output_rs, &in_proto_path).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Codegen {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use genco::prelude::*;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_can_generate_output_rust() {
|
||||
async fn test_can_generate_output_rust() -> anyhow::Result<()> {
|
||||
// Generate from protobuf
|
||||
let proto_spec = r#"
|
||||
syntax = "proto3";
|
||||
|
||||
import "includes/test_include.proto";
|
||||
|
||||
package test.can.generate.output.rust;
|
||||
|
||||
message MyEvent {
|
||||
string name = 1;
|
||||
}
|
||||
"#;
|
||||
|
||||
let proto_include_spec = r#"
|
||||
syntax = "proto3";
|
||||
|
||||
package test.can.generate.output.rust.include.test_include;
|
||||
|
||||
message MyInclude {
|
||||
string name = 1;
|
||||
}
|
||||
"#;
|
||||
|
||||
let out_tempdir = tempfile::TempDir::new()?;
|
||||
let in_tempdir = tempfile::TempDir::new()?;
|
||||
|
||||
let proto_path = in_tempdir.path().join("test.proto");
|
||||
let mut proto_file = tokio::fs::File::create(&proto_path).await?;
|
||||
proto_file.write_all(proto_spec.as_bytes()).await?;
|
||||
proto_file.sync_all().await?;
|
||||
|
||||
tokio::fs::create_dir_all(in_tempdir.path().join("includes")).await?;
|
||||
let proto_include_path = in_tempdir.path().join("includes/test_include.proto");
|
||||
let mut proto_file = tokio::fs::File::create(&proto_include_path).await?;
|
||||
proto_file.write_all(proto_include_spec.as_bytes()).await?;
|
||||
proto_file.sync_all().await?;
|
||||
|
||||
let out_tempdir_path = out_tempdir.into_path();
|
||||
let handle = tokio::task::spawn_blocking({
|
||||
let out_tempdir_path = out_tempdir_path.clone();
|
||||
move || {
|
||||
prost_build::Config::new()
|
||||
.out_dir(out_tempdir_path)
|
||||
.compile_protos(&[proto_path, proto_include_path], &[in_tempdir.into_path()])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
});
|
||||
|
||||
let result: anyhow::Result<()> = handle.await?;
|
||||
result?;
|
||||
|
||||
let mut entries = tokio::fs::read_dir(&out_tempdir_path).await?;
|
||||
let mut file_paths = Vec::new();
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
if let Some(ext) = entry.path().extension().and_then(|e| e.to_str()) {
|
||||
if ext == "rs" {
|
||||
file_paths.push(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate mod.rs
|
||||
let mod_path = out_tempdir_path.join("mod.rs");
|
||||
let mut mod_file = tokio::fs::File::create(&mod_path).await?;
|
||||
|
||||
let mut includes: Vec<genco::lang::rust::Tokens> = Vec::new();
|
||||
for generated_file in &file_paths {
|
||||
if let Some(name) = generated_file.file_name() {
|
||||
let mod_name = generated_file
|
||||
.file_stem()
|
||||
.unwrap()
|
||||
.to_ascii_lowercase()
|
||||
.to_string_lossy()
|
||||
.replace(".", "_")
|
||||
.replace("-", "_");
|
||||
|
||||
let file_name = name.to_str().unwrap();
|
||||
|
||||
includes.push(genco::quote! {
|
||||
pub mod $(mod_name) {
|
||||
include!($(quoted(file_name)));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mod_tokens: genco::lang::rust::Tokens = genco::quote! {
|
||||
$(for tokens in includes join($['\n']) => $tokens)
|
||||
};
|
||||
let mod_contents = mod_tokens.to_file_string()?;
|
||||
|
||||
pretty_assertions::assert_eq!("", mod_contents);
|
||||
|
||||
mod_file.write_all(mod_contents.as_bytes()).await?;
|
||||
|
||||
assert_eq!(1, file_paths.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user