use crate::{
GeneratedFile, ProtoMessage, ProtoMethod, ProtoService, config::WebCodegenConfig, descriptor,
error::CodegenError, error::Result,
};
pub(crate) fn parse_proto_files(config: &WebCodegenConfig) -> Result<Vec<ProtoService>> {
let set = descriptor::compile_to_descriptor_set(&config.proto_files, &config.includes)?;
let mut services = Vec::new();
for proto_path in &config.proto_files {
let file = descriptor::find_file(&set, proto_path).ok_or_else(|| {
CodegenError::proto_parse(format!(
"protoc emitted no descriptor for {}; check --include paths",
proto_path.display()
))
})?;
match descriptor::file_to_proto_service(file) {
Some(service) => services.push(service),
None => {
tracing::warn!(
"{}: no service declaration found, skipping",
proto_path.display()
);
}
}
}
Ok(services)
}
pub(crate) fn generate_rust_actors(
config: &WebCodegenConfig,
services: &[ProtoService],
) -> Result<Vec<GeneratedFile>> {
let mut files = Vec::new();
for service in services {
let file = generate_rust_actor_for_service(config, service)?;
files.push(file);
}
let mod_file = generate_rust_mod_file(config, services)?;
files.push(mod_file);
Ok(files)
}
fn generate_rust_actor_for_service(
config: &WebCodegenConfig,
service: &ProtoService,
) -> Result<GeneratedFile> {
use heck::ToSnakeCase;
let file_name = format!("{}.rs", service.name.to_snake_case());
let file_path = config.rust_output_dir.join(&file_name);
let mut content = format!(
r#"//! Auto-generated Actor code
//! Service: {}
//! Package: {}
//!
//! DO NOT EDIT this file manually
use wasm_bindgen::prelude::*;
use serde::{{Serialize, Deserialize}};
"#,
service.name, service.package
);
for message in &service.messages {
content.push_str(&generate_rust_message(message));
content.push('\n');
}
content.push_str(&format!(
r#"/// {} Actor
#[wasm_bindgen]
pub struct {}Actor {{
// Actor state
}}
#[wasm_bindgen]
impl {}Actor {{
/// Create a new Actor instance
#[wasm_bindgen(constructor)]
pub fn new() -> Self {{
Self {{}}
}}
"#,
service.name, service.name, service.name
));
for method in &service.methods {
content.push_str(&generate_rust_method(method));
content.push('\n');
}
content.push_str("}\n");
Ok(GeneratedFile::new(file_path, content))
}
fn generate_rust_message(message: &ProtoMessage) -> String {
let mut content = format!(
r#"/// {} message
#[derive(Debug, Clone, Serialize, Deserialize)]
#[wasm_bindgen]
pub struct {} {{
"#,
message.name, message.name
);
for field in &message.fields {
let rust_type = proto_type_to_rust(&field.field_type);
let field_type = if field.is_repeated {
format!("Vec<{}>", rust_type)
} else if field.is_optional {
format!("Option<{}>", rust_type)
} else {
rust_type
};
content.push_str(&format!(" pub {}: {},\n", field.name, field_type));
}
content.push_str("}\n");
content
}
fn generate_rust_method(method: &ProtoMethod) -> String {
use heck::ToSnakeCase;
let method_name = method.name.to_snake_case();
let input_type = &method.input_type;
let output_type = &method.output_type;
if method.is_streaming {
format!(
r#" /// {} method (streaming)
pub async fn {}(&self, request: {}) -> Result<JsValue, JsValue> {{
// TODO: implement streaming method
todo!("implement streaming method: {}")
}}
"#,
method.name, method_name, input_type, method.name
)
} else {
format!(
r#" /// {} method
pub async fn {}(&self, request: {}) -> Result<{}, JsValue> {{
// TODO: implement method logic
todo!("implement method: {}")
}}
"#,
method.name, method_name, input_type, output_type, method.name
)
}
}
fn proto_type_to_rust(proto_type: &str) -> String {
match proto_type {
"string" => "String".to_string(),
"bytes" => "Vec<u8>".to_string(),
"int32" | "sint32" | "sfixed32" => "i32".to_string(),
"int64" | "sint64" | "sfixed64" => "i64".to_string(),
"uint32" | "fixed32" => "u32".to_string(),
"uint64" | "fixed64" => "u64".to_string(),
"bool" => "bool".to_string(),
"float" => "f32".to_string(),
"double" => "f64".to_string(),
custom => custom.to_string(),
}
}
fn generate_rust_mod_file(
config: &WebCodegenConfig,
services: &[ProtoService],
) -> Result<GeneratedFile> {
use heck::ToSnakeCase;
let file_path = config.rust_output_dir.join("mod.rs");
let mut content = String::from(
r#"//! Auto-generated module
//!
//! DO NOT EDIT this file manually
"#,
);
for service in services {
let module_name = service.name.to_snake_case();
content.push_str(&format!("pub mod {};\n", module_name));
}
content.push('\n');
for service in services {
let module_name = service.name.to_snake_case();
content.push_str(&format!(
"pub use {}::{}Actor;\n",
module_name, service.name
));
}
Ok(GeneratedFile::new(file_path, content))
}