use std::path::PathBuf;
pub(crate) mod codegen;
mod config;
pub mod descriptor;
mod error;
mod generator;
mod request;
mod templates;
mod typescript;
pub use codegen::generate;
pub use config::{WebCodegenConfig, WebCodegenConfigBuilder};
pub(crate) use error::Result;
pub use request::WebCodegenRequest;
pub struct WebCodegen {
config: WebCodegenConfig,
}
impl WebCodegen {
pub fn new(config: WebCodegenConfig) -> Self {
Self { config }
}
pub fn generate(&self) -> Result<GeneratedFiles> {
tracing::info!("Starting actr-web code generation");
let mut files = GeneratedFiles::default();
let services = self.parse_proto_files()?;
tracing::info!("Parsed {} services", services.len());
tracing::info!("Generating Rust WASM code");
files.rust_files = self.generate_rust_actors(&services)?;
tracing::info!("Generating TypeScript types");
files.ts_types = self.generate_typescript_types(&services)?;
tracing::info!("Generating ActorRef wrappers");
files.ts_actor_refs = self.generate_actor_refs(&services)?;
if self.config.generate_react_hooks {
tracing::info!("Generating React Hooks");
files.react_hooks = self.generate_react_hooks(&services)?;
}
files.write_to_disk()?;
if self.config.format_code {
files.format_code()?;
}
tracing::info!(
"Code generation finished. Generated {} files",
files.total_count()
);
Ok(files)
}
pub fn generate_rust_only(&self) -> Result<Vec<GeneratedFile>> {
let services = self.parse_proto_files()?;
self.generate_rust_actors(&services)
}
pub fn generate_typescript_only(&self) -> Result<Vec<GeneratedFile>> {
let services = self.parse_proto_files()?;
self.generate_typescript_from_services(&services)
}
pub fn generate_typescript_from_services(
&self,
services: &[ProtoService],
) -> Result<Vec<GeneratedFile>> {
let mut files = Vec::new();
files.extend(self.generate_typescript_types(services)?);
files.extend(self.generate_actor_refs(services)?);
Ok(files)
}
fn parse_proto_files(&self) -> Result<Vec<ProtoService>> {
generator::parse_proto_files(&self.config)
}
fn generate_rust_actors(&self, services: &[ProtoService]) -> Result<Vec<GeneratedFile>> {
generator::generate_rust_actors(&self.config, services)
}
fn generate_typescript_types(&self, services: &[ProtoService]) -> Result<Vec<GeneratedFile>> {
typescript::generate_types(&self.config, services)
}
fn generate_actor_refs(&self, services: &[ProtoService]) -> Result<Vec<GeneratedFile>> {
typescript::generate_actor_refs(&self.config, services)
}
fn generate_react_hooks(&self, services: &[ProtoService]) -> Result<Vec<GeneratedFile>> {
typescript::generate_react_hooks(&self.config, services)
}
}
#[derive(Default, Debug)]
pub struct GeneratedFiles {
pub rust_files: Vec<GeneratedFile>,
pub ts_types: Vec<GeneratedFile>,
pub ts_actor_refs: Vec<GeneratedFile>,
pub react_hooks: Vec<GeneratedFile>,
}
impl GeneratedFiles {
pub fn all_files(&self) -> impl Iterator<Item = &GeneratedFile> {
self.rust_files
.iter()
.chain(self.ts_types.iter())
.chain(self.ts_actor_refs.iter())
.chain(self.react_hooks.iter())
}
pub fn total_count(&self) -> usize {
self.rust_files.len()
+ self.ts_types.len()
+ self.ts_actor_refs.len()
+ self.react_hooks.len()
}
pub fn write_to_disk(&self) -> Result<()> {
for file in self.all_files() {
file.write_to_disk()?;
}
Ok(())
}
pub fn format_code(&self) -> Result<()> {
tracing::info!("Formatting generated code");
for file in &self.rust_files {
if file.path.extension().and_then(|s| s.to_str()) == Some("rs") {
format_rust_file(&file.path)?;
}
}
let ts_files: Vec<_> = self
.ts_types
.iter()
.chain(self.ts_actor_refs.iter())
.chain(self.react_hooks.iter())
.collect();
for file in ts_files {
if file.path.extension().and_then(|s| s.to_str()) == Some("ts") {
format_typescript_file(&file.path)?;
}
}
tracing::info!("Generated code formatting completed");
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct GeneratedFile {
pub path: PathBuf,
pub content: String,
}
impl GeneratedFile {
pub fn new(path: PathBuf, content: String) -> Self {
Self { path, content }
}
pub fn write_to_disk(&self) -> Result<()> {
use std::fs;
if let Some(parent) = self.path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&self.path, &self.content)?;
tracing::debug!("Wrote file: {}", self.path.display());
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct ProtoService {
pub name: String,
pub package: String,
pub methods: Vec<ProtoMethod>,
pub messages: Vec<ProtoMessage>,
}
#[derive(Debug, Clone)]
pub struct ProtoMethod {
pub name: String,
pub input_type: String,
pub output_type: String,
pub is_streaming: bool,
}
#[derive(Debug, Clone)]
pub struct ProtoMessage {
pub name: String,
pub fields: Vec<ProtoField>,
}
#[derive(Debug, Clone)]
pub struct ProtoField {
pub name: String,
pub field_type: String,
pub number: u32,
pub is_repeated: bool,
pub is_optional: bool,
}
fn format_rust_file(path: &std::path::Path) -> Result<()> {
use std::process::Command;
let output = Command::new("rustfmt")
.arg("--edition")
.arg("2021")
.arg(path)
.output();
match output {
Ok(output) if output.status.success() => {
tracing::debug!("Formatted Rust file: {}", path.display());
Ok(())
}
Ok(output) => {
tracing::warn!(
"rustfmt failed: {}",
String::from_utf8_lossy(&output.stderr)
);
Ok(()) }
Err(e) => {
tracing::warn!("rustfmt not found or failed to execute: {}", e);
Ok(()) }
}
}
fn format_typescript_file(path: &std::path::Path) -> Result<()> {
use std::process::Command;
let output = Command::new("npx")
.args(["prettier", "--write", path.to_str().unwrap()])
.output();
match output {
Ok(output) if output.status.success() => {
tracing::debug!("Formatted TypeScript file: {}", path.display());
Ok(())
}
Ok(output) => {
tracing::warn!(
"prettier failed: {}",
String::from_utf8_lossy(&output.stderr)
);
Ok(())
}
Err(_) => {
let output = Command::new("dprint")
.args(["fmt", path.to_str().unwrap()])
.output();
match output {
Ok(output) if output.status.success() => {
tracing::debug!("Formatted TypeScript file with dprint: {}", path.display());
Ok(())
}
_ => {
tracing::warn!("No TypeScript formatter found (prettier/dprint)");
Ok(())
}
}
}
}
}