actr_web_protoc_codegen/
lib.rs1use std::path::PathBuf;
45
46pub(crate) mod codegen;
47mod config;
48pub mod descriptor;
49mod error;
50mod generator;
51mod request;
52mod templates;
53mod typescript;
54
55pub use codegen::generate;
56pub use config::{WebCodegenConfig, WebCodegenConfigBuilder};
57pub(crate) use error::Result;
58pub use request::WebCodegenRequest;
59
60pub struct WebCodegen {
62 config: WebCodegenConfig,
63}
64
65impl WebCodegen {
66 pub fn new(config: WebCodegenConfig) -> Self {
68 Self { config }
69 }
70
71 pub fn generate(&self) -> Result<GeneratedFiles> {
73 tracing::info!("Starting actr-web code generation");
74
75 let mut files = GeneratedFiles::default();
76
77 let services = self.parse_proto_files()?;
79 tracing::info!("Parsed {} services", services.len());
80
81 tracing::info!("Generating Rust WASM code");
83 files.rust_files = self.generate_rust_actors(&services)?;
84
85 tracing::info!("Generating TypeScript types");
87 files.ts_types = self.generate_typescript_types(&services)?;
88
89 tracing::info!("Generating ActorRef wrappers");
91 files.ts_actor_refs = self.generate_actor_refs(&services)?;
92
93 if self.config.generate_react_hooks {
95 tracing::info!("Generating React Hooks");
96 files.react_hooks = self.generate_react_hooks(&services)?;
97 }
98
99 files.write_to_disk()?;
101
102 if self.config.format_code {
104 files.format_code()?;
105 }
106
107 tracing::info!(
108 "Code generation finished. Generated {} files",
109 files.total_count()
110 );
111
112 Ok(files)
113 }
114
115 pub fn generate_rust_only(&self) -> Result<Vec<GeneratedFile>> {
117 let services = self.parse_proto_files()?;
118 self.generate_rust_actors(&services)
119 }
120
121 pub fn generate_typescript_only(&self) -> Result<Vec<GeneratedFile>> {
123 let services = self.parse_proto_files()?;
124 self.generate_typescript_from_services(&services)
125 }
126
127 pub fn generate_typescript_from_services(
133 &self,
134 services: &[ProtoService],
135 ) -> Result<Vec<GeneratedFile>> {
136 let mut files = Vec::new();
137 files.extend(self.generate_typescript_types(services)?);
138 files.extend(self.generate_actor_refs(services)?);
139 Ok(files)
140 }
141
142 fn parse_proto_files(&self) -> Result<Vec<ProtoService>> {
144 generator::parse_proto_files(&self.config)
145 }
146
147 fn generate_rust_actors(&self, services: &[ProtoService]) -> Result<Vec<GeneratedFile>> {
149 generator::generate_rust_actors(&self.config, services)
150 }
151
152 fn generate_typescript_types(&self, services: &[ProtoService]) -> Result<Vec<GeneratedFile>> {
154 typescript::generate_types(&self.config, services)
155 }
156
157 fn generate_actor_refs(&self, services: &[ProtoService]) -> Result<Vec<GeneratedFile>> {
159 typescript::generate_actor_refs(&self.config, services)
160 }
161
162 fn generate_react_hooks(&self, services: &[ProtoService]) -> Result<Vec<GeneratedFile>> {
164 typescript::generate_react_hooks(&self.config, services)
165 }
166}
167
168#[derive(Default, Debug)]
170pub struct GeneratedFiles {
171 pub rust_files: Vec<GeneratedFile>,
172 pub ts_types: Vec<GeneratedFile>,
173 pub ts_actor_refs: Vec<GeneratedFile>,
174 pub react_hooks: Vec<GeneratedFile>,
175}
176
177impl GeneratedFiles {
178 pub fn all_files(&self) -> impl Iterator<Item = &GeneratedFile> {
180 self.rust_files
181 .iter()
182 .chain(self.ts_types.iter())
183 .chain(self.ts_actor_refs.iter())
184 .chain(self.react_hooks.iter())
185 }
186
187 pub fn total_count(&self) -> usize {
189 self.rust_files.len()
190 + self.ts_types.len()
191 + self.ts_actor_refs.len()
192 + self.react_hooks.len()
193 }
194
195 pub fn write_to_disk(&self) -> Result<()> {
197 for file in self.all_files() {
198 file.write_to_disk()?;
199 }
200 Ok(())
201 }
202
203 pub fn format_code(&self) -> Result<()> {
205 tracing::info!("Formatting generated code");
206
207 for file in &self.rust_files {
209 if file.path.extension().and_then(|s| s.to_str()) == Some("rs") {
210 format_rust_file(&file.path)?;
211 }
212 }
213
214 let ts_files: Vec<_> = self
216 .ts_types
217 .iter()
218 .chain(self.ts_actor_refs.iter())
219 .chain(self.react_hooks.iter())
220 .collect();
221
222 for file in ts_files {
223 if file.path.extension().and_then(|s| s.to_str()) == Some("ts") {
224 format_typescript_file(&file.path)?;
225 }
226 }
227
228 tracing::info!("Generated code formatting completed");
229 Ok(())
230 }
231}
232
233#[derive(Debug, Clone)]
235pub struct GeneratedFile {
236 pub path: PathBuf,
237 pub content: String,
238}
239
240impl GeneratedFile {
241 pub fn new(path: PathBuf, content: String) -> Self {
243 Self { path, content }
244 }
245
246 pub fn write_to_disk(&self) -> Result<()> {
248 use std::fs;
249
250 if let Some(parent) = self.path.parent() {
252 fs::create_dir_all(parent)?;
253 }
254
255 fs::write(&self.path, &self.content)?;
257 tracing::debug!("Wrote file: {}", self.path.display());
258
259 Ok(())
260 }
261}
262
263#[derive(Debug, Clone)]
265pub struct ProtoService {
266 pub name: String,
267 pub package: String,
268 pub methods: Vec<ProtoMethod>,
269 pub messages: Vec<ProtoMessage>,
270}
271
272#[derive(Debug, Clone)]
274pub struct ProtoMethod {
275 pub name: String,
276 pub input_type: String,
277 pub output_type: String,
278 pub is_streaming: bool,
279}
280
281#[derive(Debug, Clone)]
283pub struct ProtoMessage {
284 pub name: String,
285 pub fields: Vec<ProtoField>,
286}
287
288#[derive(Debug, Clone)]
290pub struct ProtoField {
291 pub name: String,
292 pub field_type: String,
293 pub number: u32,
294 pub is_repeated: bool,
295 pub is_optional: bool,
296}
297
298fn format_rust_file(path: &std::path::Path) -> Result<()> {
300 use std::process::Command;
301
302 let output = Command::new("rustfmt")
303 .arg("--edition")
304 .arg("2021")
305 .arg(path)
306 .output();
307
308 match output {
309 Ok(output) if output.status.success() => {
310 tracing::debug!("Formatted Rust file: {}", path.display());
311 Ok(())
312 }
313 Ok(output) => {
314 tracing::warn!(
315 "rustfmt failed: {}",
316 String::from_utf8_lossy(&output.stderr)
317 );
318 Ok(()) }
320 Err(e) => {
321 tracing::warn!("rustfmt not found or failed to execute: {}", e);
322 Ok(()) }
324 }
325}
326
327fn format_typescript_file(path: &std::path::Path) -> Result<()> {
329 use std::process::Command;
330
331 let output = Command::new("npx")
333 .args(["prettier", "--write", path.to_str().unwrap()])
334 .output();
335
336 match output {
337 Ok(output) if output.status.success() => {
338 tracing::debug!("Formatted TypeScript file: {}", path.display());
339 Ok(())
340 }
341 Ok(output) => {
342 tracing::warn!(
343 "prettier failed: {}",
344 String::from_utf8_lossy(&output.stderr)
345 );
346 Ok(())
347 }
348 Err(_) => {
349 let output = Command::new("dprint")
351 .args(["fmt", path.to_str().unwrap()])
352 .output();
353
354 match output {
355 Ok(output) if output.status.success() => {
356 tracing::debug!("Formatted TypeScript file with dprint: {}", path.display());
357 Ok(())
358 }
359 _ => {
360 tracing::warn!("No TypeScript formatter found (prettier/dprint)");
361 Ok(())
362 }
363 }
364 }
365 }
366}