1use crate::error::Result;
2use crate::templates;
3use rohas_parser::{Api, Event, FieldType, Model, Schema, WebSocket};
4use std::fs;
5use std::path::Path;
6
7pub fn generate_models(schema: &Schema, output_dir: &Path) -> Result<()> {
8 let models_dir = output_dir.join("generated/models");
9
10 for model in &schema.models {
11 let content = generate_model_content(model);
12 let file_name = format!("{}.rs", templates::to_snake_case(&model.name));
13 fs::write(models_dir.join(file_name), content)?;
14 }
15
16 let mut mod_content = String::new();
17 mod_content.push_str("// Auto-generated module declarations\n");
18 for model in &schema.models {
19 let mod_name = templates::to_snake_case(&model.name);
20 mod_content.push_str(&format!("pub mod {};\n", mod_name));
21 mod_content.push_str(&format!("pub use {}::{};\n", mod_name, model.name));
22 }
23 fs::write(models_dir.join("mod.rs"), mod_content)?;
24
25 Ok(())
26}
27
28fn generate_model_content(model: &Model) -> String {
29 let mut content = String::new();
30
31 content.push_str("use serde::{Deserialize, Serialize};\n\n");
32 content.push_str(&format!("#[derive(Debug, Clone, Serialize, Deserialize)]\n"));
33 content.push_str(&format!("pub struct {}\n", model.name));
34 content.push_str("{\n");
35
36 for field in &model.fields {
37 let rust_type = field.field_type.to_rust();
38 let type_hint = if field.optional {
39 format!("Option<{}>", rust_type)
40 } else {
41 rust_type
42 };
43
44 let field_name = &field.name;
45 content.push_str(&format!(" pub {}: {},\n", field_name, type_hint));
46 }
47
48 if model.fields.is_empty() {
49 content.push_str(" // No fields\n");
50 }
51
52 content.push_str("}\n");
53
54 content
55}
56
57pub fn generate_dtos(schema: &Schema, output_dir: &Path) -> Result<()> {
58 let dto_dir = output_dir.join("generated/dto");
59
60 for input in &schema.inputs {
61 let content = generate_model_content(&rohas_parser::Model {
62 name: input.name.clone(),
63 fields: input.fields.clone(),
64 attributes: vec![],
65 });
66 let file_name = format!("{}.rs", templates::to_snake_case(&input.name));
67 fs::write(dto_dir.join(file_name), content)?;
68 }
69
70 let mut mod_content = String::new();
71 mod_content.push_str("// Auto-generated module declarations\n");
72 for input in &schema.inputs {
73 let mod_name = templates::to_snake_case(&input.name);
74 mod_content.push_str(&format!("pub mod {};\n", mod_name));
75 mod_content.push_str(&format!("pub use {}::{};\n", mod_name, input.name));
76 }
77 fs::write(dto_dir.join("mod.rs"), mod_content)?;
78
79 Ok(())
80}
81
82pub fn generate_apis(schema: &Schema, output_dir: &Path) -> Result<()> {
83 let api_dir = output_dir.join("generated/api");
84
85 for api in &schema.apis {
86 let content = generate_api_content(api);
87 let file_name = format!("{}.rs", templates::to_snake_case(&api.name));
88 fs::write(api_dir.join(file_name), content)?;
89 }
90
91 let mut mod_content = String::new();
92 mod_content.push_str("// Auto-generated module declarations\n");
93 for api in &schema.apis {
94 let mod_name = templates::to_snake_case(&api.name);
95 mod_content.push_str(&format!("pub mod {};\n", mod_name));
96 mod_content.push_str(&format!("pub use {}::{{ {}Request, {}Response }};\n", mod_name, api.name, api.name));
97 }
98 fs::write(api_dir.join("mod.rs"), mod_content)?;
99
100 let handlers_dir = output_dir.join("handlers/api");
101 for api in &schema.apis {
102 let file_name = format!("{}.rs", templates::to_snake_case(&api.name));
103 let handler_path = handlers_dir.join(&file_name);
104
105 if !handler_path.exists() {
106 let content = generate_api_handler_stub(api);
107 fs::write(handler_path, content)?;
108 }
109 }
110
111 Ok(())
112}
113
114fn generate_api_content(api: &Api) -> String {
115 let mut content = String::new();
116
117 content.push_str("use serde::{Deserialize, Serialize};\n");
118
119 if let Some(body_type) = &api.body {
120 let body_type_snake = templates::to_snake_case(body_type);
121 if body_type.ends_with("Input") {
122 content.push_str(&format!("use super::super::dto::{}::{};\n", body_type_snake, body_type));
123 } else {
124 content.push_str(&format!("use super::super::models::{}::{};\n", body_type_snake, body_type));
125 }
126 }
127
128 let response_field_type = rohas_parser::FieldType::from_str(&api.response);
129 let is_custom_response = matches!(response_field_type, rohas_parser::FieldType::Custom(_));
130 if is_custom_response {
131 let response_type_snake = templates::to_snake_case(&api.response);
132 content.push_str(&format!("use super::super::models::{}::{};\n", response_type_snake, api.response));
133 }
134 content.push_str("\n");
135
136 if let Some(body_type) = &api.body {
137 content.push_str(&format!(
138 "pub type {}Request = {};\n\n",
139 api.name, body_type
140 ));
141 } else {
142 content.push_str(&format!(
143 "#[derive(Debug, Clone, Serialize, Deserialize)]\n"
144 ));
145 content.push_str(&format!("pub struct {}Request\n", api.name));
146 content.push_str("{\n");
147 content.push_str(" // No body fields\n");
148 content.push_str("}\n\n");
149 }
150
151 let response_rust_type = response_field_type.to_rust();
152 content.push_str(&format!(
153 "pub type {}Response = {};\n",
154 api.name, response_rust_type
155 ));
156
157 content
158}
159
160fn generate_api_handler_stub(api: &Api) -> String {
161 let mut content = String::new();
162
163 let request_type = format!("{}Request", api.name);
164 let response_type = format!("{}Response", api.name);
165 let handler_name = format!("handle_{}", templates::to_snake_case(&api.name));
166 let module_name = templates::to_snake_case(&api.name);
167
168 content.push_str(&format!(
169 "use crate::generated::api::{}::{{ {}, {} }};\n",
170 module_name, request_type, response_type
171 ));
172 content.push_str("use crate::generated::state::State;\n");
173 content.push_str("use rohas_runtime::{HandlerContext, HandlerResult, Result};\n\n");
174
175 content.push_str(&format!(
176 "/// Rust handler for {} API.\n",
177 api.name
178 ));
179 content.push_str(&format!(
180 "pub async fn {}(\n",
181 handler_name
182 ));
183 content.push_str(&format!(" req: {},\n", request_type));
184 content.push_str(" state: &mut State,\n");
185 content.push_str(&format!(") -> Result<{}> {{\n", response_type));
186 content.push_str(" // TODO: Implement handler logic\n");
187 content.push_str(" // For auto-triggers (defined in schema triggers): use state.set_payload(\"EventName\", value)\n");
188 content.push_str(" // For manual triggers: use state.trigger_event(\"EventName\", value)\n");
189 content.push_str(" // Use state.logger for structured logging\n");
190 content.push_str(&format!(
191 " Err(rohas_runtime::RuntimeError::ExecutionFailed(\"Handler not implemented\".into()))\n"
192 ));
193 content.push_str("}\n");
194
195 content
196}
197
198pub fn generate_events(schema: &Schema, output_dir: &Path) -> Result<()> {
199 let events_dir = output_dir.join("generated/events");
200
201 for event in &schema.events {
202 let content = generate_event_content(event);
203 let file_name = format!("{}.rs", templates::to_snake_case(&event.name));
204 fs::write(events_dir.join(file_name), content)?;
205 }
206
207 let mut mod_content = String::new();
208 mod_content.push_str("// Auto-generated module declarations\n");
209 for event in &schema.events {
210 let mod_name = templates::to_snake_case(&event.name);
211 mod_content.push_str(&format!("pub mod {};\n", mod_name));
212 mod_content.push_str(&format!("pub use {}::{};\n", mod_name, event.name));
213 }
214 fs::write(events_dir.join("mod.rs"), mod_content)?;
215
216 let handlers_dir = output_dir.join("handlers/events");
217 for event in &schema.events {
218 for handler in &event.handlers {
219 let file_name = format!("{}.rs", handler);
220 let handler_path = handlers_dir.join(&file_name);
221
222 if !handler_path.exists() {
223 let content = generate_event_handler_stub(event, handler);
224 fs::write(handler_path, content)?;
225 }
226 }
227 }
228
229 Ok(())
230}
231
232fn generate_event_content(event: &Event) -> String {
233 let mut content = String::new();
234
235 content.push_str("use serde::{Deserialize, Serialize};\n");
236 content.push_str("use chrono::{DateTime, Utc};\n\n");
237
238 let payload_field_type = FieldType::from_str(&event.payload);
239 let payload_rust_type = payload_field_type.to_rust();
240
241 let is_custom_type = matches!(payload_field_type, FieldType::Custom(_));
242 if is_custom_type {
243 let model_module = templates::to_snake_case(&event.payload);
244 content.push_str(&format!(
245 "use crate::generated::models::{}::{};\n",
246 model_module, event.payload
247 ));
248 }
249
250 content.push_str(&format!("#[derive(Debug, Clone, Serialize, Deserialize)]\n"));
251 content.push_str(&format!("pub struct {}\n", event.name));
252 content.push_str("{\n");
253 content.push_str(&format!(" pub payload: {},\n", payload_rust_type));
254 content.push_str(" pub timestamp: DateTime<Utc>,\n");
255 content.push_str("}\n");
256
257 content
258}
259
260fn generate_event_handler_stub(event: &Event, handler_name: &str) -> String {
261 let mut content = String::new();
262
263 let event_module = templates::to_snake_case(&event.name);
264
265 content.push_str(&format!(
266 "use crate::generated::events::{}::{};\n",
267 event_module, event.name
268 ));
269 content.push_str("use rohas_runtime::{HandlerContext, HandlerResult, Result};\n\n");
270
271 content.push_str(&format!(
272 "/// High-performance Rust event handler.\n"
273 ));
274 content.push_str(&format!(
275 "pub async fn {}(\n",
276 handler_name
277 ));
278 content.push_str(&format!(" event: {},\n", event.name));
279 content.push_str(") -> Result<HandlerResult> {\n");
280 content.push_str(" // TODO: Implement event handler\n");
281 content.push_str(&format!(
282 " tracing::info!(\"Handling event: {{:?}}\", event);\n"
283 ));
284 content.push_str(" Ok(HandlerResult::success(serde_json::json!({}), 0))\n");
285 content.push_str("}\n");
286
287 content
288}
289
290pub fn generate_crons(schema: &Schema, output_dir: &Path) -> Result<()> {
291 let handlers_dir = output_dir.join("handlers/cron");
292
293 for cron in &schema.crons {
294 let file_name = format!("{}.rs", templates::to_snake_case(&cron.name));
295 let handler_path = handlers_dir.join(&file_name);
296
297 if !handler_path.exists() {
298 let content = generate_cron_handler_stub(cron);
299 fs::write(handler_path, content)?;
300 }
301 }
302
303 Ok(())
304}
305
306fn generate_cron_handler_stub(cron: &rohas_parser::Cron) -> String {
307 let mut content = String::new();
308
309 let handler_name = format!("handle_{}", templates::to_snake_case(&cron.name));
310
311 content.push_str("use rohas_runtime::{HandlerContext, HandlerResult, Result};\n");
312 content.push_str("use crate::generated::state::State;\n\n");
313
314 content.push_str(&format!(
315 "/// High-performance Rust cron handler.\n"
316 ));
317 content.push_str(&format!(
318 "pub async fn {}(\n",
319 handler_name
320 ));
321 content.push_str(" state: &mut State,\n");
322 content.push_str(") -> Result<HandlerResult> {\n");
323 content.push_str(" // TODO: Implement cron handler\n");
324 content.push_str(&format!(
325 " tracing::info!(\"Executing cron: {}\");\n",
326 cron.name
327 ));
328 content.push_str(" Ok(HandlerResult::success(serde_json::json!({}), 0))\n");
329 content.push_str("}\n");
330
331 content
332}
333
334pub fn generate_websockets(schema: &Schema, output_dir: &Path) -> Result<()> {
335 let ws_dir = output_dir.join("generated/websockets");
336
337 for ws in &schema.websockets {
338 let content = generate_websocket_content(ws);
339 let file_name = format!("{}.rs", templates::to_snake_case(&ws.name));
340 fs::write(ws_dir.join(file_name), content)?;
341 }
342
343 let mut mod_content = String::new();
344 mod_content.push_str("// Auto-generated module declarations\n");
345 for ws in &schema.websockets {
346 let mod_name = templates::to_snake_case(&ws.name);
347 mod_content.push_str(&format!("pub mod {};\n", mod_name));
348 mod_content.push_str(&format!("pub use {}::{{ {}Connection", mod_name, ws.name));
349 if ws.message.is_some() {
350 mod_content.push_str(&format!(", {}Message", ws.name));
351 }
352 mod_content.push_str(" };\n");
353 }
354 fs::write(ws_dir.join("mod.rs"), mod_content)?;
355
356 let handlers_dir = output_dir.join("handlers/websockets");
357 for ws in &schema.websockets {
358 for handler in &ws.on_connect {
359 let file_name = format!("{}.rs", handler);
360 let handler_path = handlers_dir.join(&file_name);
361 if !handler_path.exists() {
362 let content = generate_websocket_handler_stub(ws, handler, "connect");
363 fs::write(handler_path, content)?;
364 }
365 }
366 for handler in &ws.on_message {
367 let file_name = format!("{}.rs", handler);
368 let handler_path = handlers_dir.join(&file_name);
369 if !handler_path.exists() {
370 let content = generate_websocket_handler_stub(ws, handler, "message");
371 fs::write(handler_path, content)?;
372 }
373 }
374 for handler in &ws.on_disconnect {
375 let file_name = format!("{}.rs", handler);
376 let handler_path = handlers_dir.join(&file_name);
377 if !handler_path.exists() {
378 let content = generate_websocket_handler_stub(ws, handler, "disconnect");
379 fs::write(handler_path, content)?;
380 }
381 }
382 }
383
384 Ok(())
385}
386
387fn generate_websocket_content(ws: &WebSocket) -> String {
388 let mut content = String::new();
389
390 content.push_str("use serde::{Deserialize, Serialize};\n\n");
391
392 if let Some(message_type) = &ws.message {
393 let rust_type = FieldType::from_str(message_type).to_rust();
394
395 content.push_str(&format!(
396 "pub type {}Message = {};\n\n",
397 ws.name, rust_type
398 ));
399 }
400
401 content.push_str(&format!(
402 "#[derive(Debug, Clone, Serialize, Deserialize)]\n"
403 ));
404 content.push_str(&format!("pub struct {}Connection\n", ws.name));
405 content.push_str("{\n");
406 content.push_str(" // Connection metadata\n");
407 content.push_str("}\n");
408
409 content
410}
411
412fn generate_websocket_handler_stub(ws: &WebSocket, handler_name: &str, event_type: &str) -> String {
413 let mut content = String::new();
414
415 let ws_module = templates::to_snake_case(&ws.name);
416
417 content.push_str(&format!(
418 "use crate::generated::websockets::{}::{}Connection;\n",
419 ws_module, ws.name
420 ));
421
422 if ws.message.is_some() {
423 content.push_str(&format!(
424 "use crate::generated::websockets::{}::{}Message;\n",
425 ws_module, ws.name
426 ));
427 }
428
429 content.push_str("use rohas_runtime::{HandlerContext, HandlerResult, Result};\n");
430 content.push_str("use crate::generated::state::State;\n\n");
431
432 content.push_str(&format!(
433 "/// Rust WebSocket {} handler.\n",
434 event_type
435 ));
436 content.push_str(&format!("pub async fn {}(\n", handler_name));
437
438 if event_type == "message" {
439 if let Some(_) = &ws.message {
440 content.push_str(&format!(" message: {}Message,\n", ws.name));
441 }
442 content.push_str(&format!(" connection: {}Connection,\n", ws.name));
443 content.push_str(" state: &mut State,\n");
444 } else {
445 content.push_str(&format!(" connection: {}Connection,\n", ws.name));
446 if event_type == "connect" {
447 content.push_str(" state: &mut State,\n");
448 }
449 }
450
451 content.push_str(") -> Result<HandlerResult> {\n");
452 content.push_str(&format!(
453 " tracing::info!(\"WebSocket {} handler: {{:?}}\", connection);\n",
454 event_type
455 ));
456 content.push_str(" Ok(HandlerResult::success(serde_json::json!({}), 0))\n");
457 content.push_str("}\n");
458
459 content
460}
461
462pub fn generate_middlewares(schema: &Schema, output_dir: &Path) -> Result<()> {
463 let mut middleware_names = std::collections::HashSet::new();
464
465 for api in &schema.apis {
466 for mw in &api.middlewares {
467 middleware_names.insert(mw.clone());
468 }
469 }
470
471 for ws in &schema.websockets {
472 for mw in &ws.middlewares {
473 middleware_names.insert(mw.clone());
474 }
475 }
476
477 let handlers_dir = output_dir.join("handlers/middlewares");
478 for mw_name in middleware_names {
479 let file_name = format!("{}.rs", templates::to_snake_case(&mw_name));
480 let handler_path = handlers_dir.join(&file_name);
481
482 if !handler_path.exists() {
483 let content = generate_middleware_stub(&mw_name);
484 fs::write(handler_path, content)?;
485 }
486 }
487
488 Ok(())
489}
490
491fn generate_middleware_stub(mw_name: &str) -> String {
492 let mut content = String::new();
493
494 let handler_name = format!("{}_middleware", templates::to_snake_case(mw_name));
495
496 content.push_str("use rohas_runtime::{HandlerContext, HandlerResult, Result};\n");
497 content.push_str("use crate::generated::state::State;\n\n");
498
499 content.push_str(&format!(
500 "/// High-performance Rust middleware.\n"
501 ));
502 content.push_str(&format!("pub async fn {}(\n", handler_name));
503 content.push_str(" ctx: HandlerContext,\n");
504 content.push_str(" state: &mut State,\n");
505 content.push_str(") -> Result<HandlerResult> {\n");
506 content.push_str(" // TODO: Implement middleware logic\n");
507 content.push_str(" // Return Ok to continue, Err to abort\n");
508 content.push_str(&format!(
509 " tracing::info!(\"Middleware {} executed\");\n",
510 mw_name
511 ));
512 content.push_str(" Ok(HandlerResult::success(serde_json::json!({}), 0))\n");
513 content.push_str("}\n");
514
515 content
516}
517
518pub fn generate_state(output_dir: &Path) -> Result<()> {
519 let generated_dir = output_dir.join("generated");
520 let content = r#"use serde_json::Value;
521use std::collections::HashMap;
522use tracing::{error, warn, info, debug, trace};
523
524/// State struct for Rust handlers.
525#[derive(Debug, Clone)]
526pub struct State {
527 handler_name: String,
528 triggers: Vec<TriggeredEvent>,
529 auto_trigger_payloads: HashMap<String, Value>,
530}
531
532#[derive(Debug, Clone)]
533pub struct TriggeredEvent {
534 pub event_name: String,
535 pub payload: Value,
536}
537
538impl State {
539 /// Create a new State instance.
540 pub fn new(handler_name: impl Into<String>) -> Self {
541 Self {
542 handler_name: handler_name.into(),
543 triggers: Vec::new(),
544 auto_trigger_payloads: HashMap::new(),
545 }
546 }
547
548 /// Manually trigger an event (for events NOT in schema triggers).
549 pub fn trigger_event(&mut self, event_name: impl Into<String>, payload: Value) {
550 self.triggers.push(TriggeredEvent {
551 event_name: event_name.into(),
552 payload,
553 });
554 }
555
556 /// Set payload for an auto-triggered event (for events IN schema triggers).
557 pub fn set_payload(&mut self, event_name: impl Into<String>, payload: Value) {
558 self.auto_trigger_payloads.insert(event_name.into(), payload);
559 }
560
561 /// Get all manually triggered events (internal use).
562 pub fn get_triggers(&self) -> &[TriggeredEvent] {
563 &self.triggers
564 }
565
566 /// Get all auto-trigger payloads (internal use).
567 pub fn get_all_auto_trigger_payloads(&self) -> &HashMap<String, Value> {
568 &self.auto_trigger_payloads
569 }
570
571 /// Get a logger instance for this handler.
572 pub fn logger(&self) -> Logger {
573 Logger::new(&self.handler_name)
574 }
575}
576
577/// Structured logger for handlers.
578pub struct Logger {
579 handler_name: String,
580}
581
582impl Logger {
583 pub fn new(handler_name: impl Into<String>) -> Self {
584 Self {
585 handler_name: handler_name.into(),
586 }
587 }
588
589 pub fn info(&self, message: &str) {
590 info!(handler = %self.handler_name, %message);
591 }
592
593 pub fn error(&self, message: &str) {
594 error!(handler = %self.handler_name, %message);
595 }
596
597 pub fn warn(&self, message: &str) {
598 warn!(handler = %self.handler_name, %message);
599 }
600
601 pub fn debug(&self, message: &str) {
602 debug!(handler = %self.handler_name, %message);
603 }
604
605 pub fn trace(&self, message: &str) {
606 trace!(handler = %self.handler_name, %message);
607 }
608}
609"#;
610
611 fs::write(generated_dir.join("state.rs"), content)?;
612 Ok(())
613}
614
615pub fn generate_lib_rs(schema: &Schema, output_dir: &Path) -> Result<()> {
617 let generated_dir = output_dir.join("generated");
618
619 let mut content = String::new();
620 content.push_str("// Auto-generated Rust code from Rohas schema\n");
621 content.push_str("// DO NOT EDIT MANUALLY\n\n");
622
623 content.push_str("pub mod state;\n");
625 content.push_str("pub mod models;\n");
626 content.push_str("pub mod dto;\n");
627 content.push_str("pub mod api;\n");
628 content.push_str("pub mod events;\n");
629 content.push_str("pub mod websockets;\n");
630 content.push_str("pub mod handlers;\n\n");
631
632 content.push_str("pub use state::State;\n");
634 content.push_str("pub use handlers::register_all_handlers;\n");
635 content.push_str("pub use handlers::set_runtime;\n\n");
636
637 fs::write(generated_dir.join("lib.rs"), content)?;
638
639 generate_handlers_registration(schema, output_dir)?;
641
642
643 let mut main_lib_content = String::new();
645 main_lib_content.push_str("// Main library entry point for Rohas Rust application\n");
646 main_lib_content.push_str("// This file sets up the module structure\n\n");
647 main_lib_content.push_str("#[path = \"generated/lib.rs\"]\n");
648 main_lib_content.push_str("pub mod generated;\n\n");
649 main_lib_content.push_str("// Re-export generated types for convenience\n");
650 main_lib_content.push_str("pub use generated::*;\n\n");
651
652 let handlers_dir = output_dir.join("handlers");
654 if handlers_dir.join("api").exists() || handlers_dir.join("events").exists() {
655 main_lib_content.push_str("pub mod handlers;\n\n");
656 }
657
658 main_lib_content.push_str("/// Initialize and register all handlers with the Rust runtime.\n");
660 main_lib_content.push_str("/// This function should be called during engine startup.\n");
661 main_lib_content.push_str("/// It will automatically register all handlers using the global registry.\n");
662 main_lib_content.push_str("pub async fn init_handlers(runtime: std::sync::Arc<rohas_runtime::RustRuntime>) -> rohas_runtime::Result<()> {\n");
663 main_lib_content.push_str(" generated::register_all_handlers(runtime).await\n");
664 main_lib_content.push_str("}\n\n");
665
666 main_lib_content.push_str("/// C-compatible FFI function for automatic handler registration.\n");
669 main_lib_content.push_str("/// This is called automatically by the engine.\n");
670 main_lib_content.push_str("/// Returns 0 on success, non-zero on error.\n");
671 main_lib_content.push_str("#[no_mangle]\n");
672 main_lib_content.push_str("pub extern \"C\" fn rohas_set_runtime(runtime_ptr: *mut std::ffi::c_void) -> i32 {\n");
673 main_lib_content.push_str(" use std::sync::Arc;\n");
674 main_lib_content.push_str(" \n");
675 main_lib_content.push_str(" if runtime_ptr.is_null() {\n");
676 main_lib_content.push_str(" return 1; // Error: null pointer\n");
677 main_lib_content.push_str(" }\n");
678 main_lib_content.push_str(" \n");
679 main_lib_content.push_str(" // Safety: The engine passes a valid Arc<RustRuntime> pointer that was created with Arc::into_raw.\n");
680 main_lib_content.push_str(" // We reconstruct the Arc temporarily to clone it, then forget it so the engine retains ownership.\n");
681 main_lib_content.push_str(" unsafe {\n");
682 main_lib_content.push_str(" // Convert the raw pointer back to Arc<RustRuntime>\n");
683 main_lib_content.push_str(" // The engine created this with Arc::into_raw, so we reconstruct it temporarily\n");
684 main_lib_content.push_str(" let runtime: Arc<rohas_runtime::RustRuntime> = Arc::from_raw(runtime_ptr as *const rohas_runtime::RustRuntime);\n");
685 main_lib_content.push_str(" \n");
686 main_lib_content.push_str(" // Clone the Arc - this increments the reference count\n");
687 main_lib_content.push_str(" let runtime_clone = runtime.clone();\n");
688 main_lib_content.push_str(" \n");
689 main_lib_content.push_str(" // Forget the reconstructed Arc - we don't want to drop it here since the engine still owns it\n");
690 main_lib_content.push_str(" // The engine will manage the original Arc's lifetime\n");
691 main_lib_content.push_str(" std::mem::forget(runtime);\n");
692 main_lib_content.push_str(" \n");
693 main_lib_content.push_str(" // Call the generated set_runtime function which will register all handlers\n");
694 main_lib_content.push_str(" // This will store the cloned Arc in a OnceLock and register handlers synchronously\n");
695 main_lib_content.push_str(" // Note: If registration fails, set_runtime will panic (via .expect())\n");
696 main_lib_content.push_str(" generated::set_runtime(runtime_clone);\n");
697 main_lib_content.push_str(" \n");
698 main_lib_content.push_str(" 0 // Success\n");
699 main_lib_content.push_str(" }\n");
700 main_lib_content.push_str("}\n");
701
702 fs::write(output_dir.join("lib.rs"), main_lib_content)?;
703
704 if handlers_dir.join("api").exists() || handlers_dir.join("events").exists() {
706 generate_handlers_mod(schema, output_dir)?;
707 }
708
709 Ok(())
710}
711
712fn generate_handlers_mod(schema: &Schema, output_dir: &Path) -> Result<()> {
713 let handlers_dir = output_dir.join("handlers");
714 let mut content = String::new();
715
716 content.push_str("// Handler module declarations\n\n");
717
718 if handlers_dir.join("api").exists() {
719 content.push_str("pub mod api;\n");
720 }
721
722 if handlers_dir.join("events").exists() {
723 content.push_str("pub mod events;\n");
724 }
725
726 fs::write(handlers_dir.join("mod.rs"), content)?;
727
728 if handlers_dir.join("api").exists() {
729 let mut api_mod = String::new();
730 api_mod.push_str("// API handler modules\n\n");
731
732 for api in &schema.apis {
733 let handler_name = templates::to_snake_case(&api.name);
734 let handler_file = handlers_dir.join("api").join(format!("{}.rs", handler_name));
735 if handler_file.exists() {
736 api_mod.push_str(&format!("pub mod {};\n", handler_name));
737 }
738 }
739
740 fs::write(handlers_dir.join("api").join("mod.rs"), api_mod)?;
741 }
742
743 if handlers_dir.join("events").exists() {
744 let mut events_mod = String::new();
745 events_mod.push_str("// Event handler modules\n\n");
746
747 for event in &schema.events {
748 for handler in &event.handlers {
749 let handler_file = handlers_dir.join("events").join(format!("{}.rs", handler));
750 if handler_file.exists() {
751 events_mod.push_str(&format!("pub mod {};\n", handler));
752 }
753 }
754 }
755
756 fs::write(handlers_dir.join("events").join("mod.rs"), events_mod)?;
757 }
758
759 Ok(())
760}
761
762fn generate_handlers_registration(schema: &Schema, output_dir: &Path) -> Result<()> {
763 let generated_dir = output_dir.join("generated");
764 let handlers_dir = output_dir.join("handlers");
765
766 let mut content = String::new();
767 content.push_str("// Auto-generated handler registration\n");
768 content.push_str("// DO NOT EDIT MANUALLY\n\n");
769
770 content.push_str("use rohas_runtime::{RustRuntime, HandlerContext, HandlerResult, Result};\n");
771 content.push_str("use std::sync::Arc;\n");
772 content.push_str("use std::sync::OnceLock;\n\n");
773
774 content.push_str("// Global registry for automatic handler registration\n");
775 content.push_str("static RUNTIME_REGISTRY: OnceLock<Arc<RustRuntime>> = OnceLock::new();\n\n");
776 content.push_str("/// Set the runtime for automatic handler registration.\n");
777 content.push_str("/// This is called automatically by the engine.\n");
778 content.push_str("/// This function is public so it can be called from the engine.\n");
779 content.push_str("/// Note: Each dylib has its own OnceLock, so this can be called fresh on each reload.\n");
780 content.push_str("pub fn set_runtime(runtime: Arc<RustRuntime>) {\n");
781 content.push_str(" // Set the runtime (this will only succeed once per dylib load, which is what we want)\n");
782 content.push_str(" let _ = RUNTIME_REGISTRY.set(runtime);\n");
783 content.push_str(" // Always trigger registration (important for hot reload)\n");
784 content.push_str(" register_all_handlers_internal().expect(\"Failed to register handlers\");\n");
785 content.push_str("}\n\n");
786
787 let mut has_handlers = false;
788
789 for api in &schema.apis {
790 let handler_name = templates::to_snake_case(&api.name);
791 let handler_file = handlers_dir.join("api").join(format!("{}.rs", handler_name));
792 if handler_file.exists() {
793 has_handlers = true;
794 break;
795 }
796 }
797
798 if !has_handlers {
799 for event in &schema.events {
800 for handler in &event.handlers {
801 let handler_file = handlers_dir.join("events").join(format!("{}.rs", handler));
802 if handler_file.exists() {
803 has_handlers = true;
804 break;
805 }
806 }
807 if has_handlers {
808 break;
809 }
810 }
811 }
812
813 if !has_handlers {
814 content.push_str("/// Register all handlers with the Rust runtime.\n");
815 content.push_str("/// No handlers found - implement handlers in src/handlers/ to register them.\n");
816 content.push_str("pub async fn register_all_handlers(_runtime: Arc<RustRuntime>) -> Result<()> {\n");
817 content.push_str(" Ok(())\n");
818 content.push_str("}\n\n");
819 content.push_str("fn register_all_handlers_internal() -> Result<()> {\n");
820 content.push_str(" Ok(())\n");
821 content.push_str("}\n");
822 fs::write(generated_dir.join("handlers.rs"), content)?;
823 return Ok(());
824 }
825
826 content.push_str("// Import handler functions\n");
827
828 for api in &schema.apis {
829 let handler_name = templates::to_snake_case(&api.name);
830 let handler_file = handlers_dir.join("api").join(format!("{}.rs", handler_name));
831
832 if handler_file.exists() {
833 content.push_str(&format!(
834 "use crate::handlers::api::{}::handle_{};\n",
835 handler_name, handler_name
836 ));
837 }
838 }
839
840 for event in &schema.events {
841 for handler in &event.handlers {
842 let handler_file = handlers_dir.join("events").join(format!("{}.rs", handler));
843
844 if handler_file.exists() {
845 content.push_str(&format!(
846 "use crate::handlers::events::{}::{};\n",
847 handler, handler
848 ));
849 }
850 }
851 }
852
853 content.push_str("\n");
854 content.push_str("/// Register all handlers with the Rust runtime.\n");
855 content.push_str("/// This function should be called during engine initialization.\n");
856 content.push_str("pub async fn register_all_handlers(runtime: Arc<RustRuntime>) -> Result<()> {\n");
857 content.push_str(" set_runtime(runtime);\n");
858 content.push_str(" Ok(())\n");
859 content.push_str("}\n\n");
860
861 content.push_str("/// Internal registration function (synchronous, for static initialization).\n");
862 content.push_str("fn register_all_handlers_internal() -> Result<()> {\n");
863 content.push_str(" use tracing::info;\n");
864 content.push_str(" info!(\"Registering Rust handlers from dylib...\");\n");
865 content.push_str(" let runtime = RUNTIME_REGISTRY.get().ok_or_else(|| rohas_runtime::RuntimeError::ExecutionFailed(\"Runtime not set\".into()))?;\n");
866 content.push_str(" let rt = tokio::runtime::Runtime::new().map_err(|e| rohas_runtime::RuntimeError::ExecutionFailed(e.to_string()))?;\n");
867 content.push_str(" rt.block_on(async {\n");
868
869 for api in &schema.apis {
870 let handler_name = templates::to_snake_case(&api.name);
871 let handler_file = handlers_dir.join("api").join(format!("{}.rs", handler_name));
872
873 if handler_file.exists() {
874 content.push_str(&format!(
875 " // Register API handler: {}\n",
876 api.name
877 ));
878 content.push_str(&format!(
879 " runtime.register_handler(\n"
880 ));
881 content.push_str(&format!(
882 " \"{}\".to_string(),\n",
883 handler_name
884 ));
885 content.push_str(&format!(
886 " |ctx: HandlerContext| async move {{\n"
887 ));
888 content.push_str(&format!(
889 " // Parse request from context\n"
890 ));
891 content.push_str(&format!(
892 " let req: crate::generated::api::{}::{}Request = serde_json::from_value(ctx.payload.clone())?;\n",
893 handler_name, api.name
894 ));
895 content.push_str(&format!(
896 " let mut state = crate::generated::state::State::new(&ctx.handler_name);\n"
897 ));
898 content.push_str(&format!(
899 " let response = handle_{}(req, &mut state).await?;\n",
900 handler_name
901 ));
902 content.push_str(&format!(
903 " Ok(HandlerResult::success(serde_json::to_value(response)?, 0))\n"
904 ));
905 content.push_str(&format!(
906 " }}\n"
907 ));
908 content.push_str(&format!(
909 " ).await;\n"
910 ));
911 content.push_str(&format!(
912 " info!(\"Registered handler: {}\");\n",
913 handler_name
914 ));
915 }
916 }
917
918 content.push_str(" Ok::<(), rohas_runtime::RuntimeError>(())\n");
919 content.push_str(" })?;\n");
920 content.push_str(" Ok(())\n");
921 content.push_str("}\n");
922
923 fs::write(generated_dir.join("handlers.rs"), content)?;
924 Ok(())
925}
926
927pub fn is_in_rohas_workspace(output_dir: &Path) -> bool {
928 let project_root = if output_dir.file_name().and_then(|s| s.to_str()) == Some("src") {
929 output_dir.parent().unwrap_or(output_dir)
930 } else {
931 output_dir
932 };
933
934 let path_str = project_root.to_string_lossy();
935 if path_str.contains("/examples/") || path_str.contains("\\examples\\") {
936 return true;
937 }
938
939 let mut current = project_root;
940 for _ in 0..5 {
941 let crates_dir = current.join("crates").join("rohas-cli");
942 if crates_dir.exists() {
943 return true;
944 }
945 if let Some(parent) = current.parent() {
946 current = parent;
947 } else {
948 break;
949 }
950 }
951
952 false
953}
954
955pub fn generate_dev_scripts(output_dir: &Path) -> Result<()> {
956 let project_root = if output_dir.file_name().and_then(|s| s.to_str()) == Some("src") {
957 output_dir.parent().unwrap_or(output_dir).to_path_buf()
958 } else {
959 output_dir.to_path_buf()
960 };
961
962 let dev_script = r#"#!/bin/bash
963# Development helper script for Rohas developers
964# For end users: install rohas CLI and run "rohas dev --workbench" directly
965
966set -e
967
968# Find the workspace root (look for Cargo.toml with [workspace])
969SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
970WORKSPACE_ROOT="$SCRIPT_DIR"
971
972# Look for workspace root (go up to 10 levels to handle nested examples)
973for i in {1..10}; do
974 if [ -f "$WORKSPACE_ROOT/Cargo.toml" ]; then
975 # Check if it's a workspace (has [workspace] and contains crates/rohas-cli)
976 if grep -q "^\[workspace\]" "$WORKSPACE_ROOT/Cargo.toml" 2>/dev/null && \
977 [ -d "$WORKSPACE_ROOT/crates/rohas-cli" ]; then
978 break
979 fi
980 fi
981 WORKSPACE_ROOT="$(dirname "$WORKSPACE_ROOT")"
982 # Stop if we've reached the filesystem root
983 if [ "$WORKSPACE_ROOT" = "/" ] || [ "$WORKSPACE_ROOT" = "$SCRIPT_DIR" ]; then
984 break
985 fi
986done
987
988if [ -f "$WORKSPACE_ROOT/Cargo.toml" ] && grep -q "^\[workspace\]" "$WORKSPACE_ROOT/Cargo.toml" 2>/dev/null && \
989 [ -d "$WORKSPACE_ROOT/crates/rohas-cli" ]; then
990 cd "$WORKSPACE_ROOT"
991 REL_SCHEMA_PATH=$(python3 -c "import os; print(os.path.relpath('$SCRIPT_DIR/schema', '$WORKSPACE_ROOT'))" 2>/dev/null || \
992 perl -MFile::Spec -e "print File::Spec->abs2rel('$SCRIPT_DIR/schema', '$WORKSPACE_ROOT')" 2>/dev/null || \
993 echo "schema")
994 # Check if --schema argument is already provided
995 HAS_SCHEMA_ARG=false
996 for arg in "$@"; do
997 if [[ "$arg" == "--schema" ]] || [[ "$arg" == "-s" ]]; then
998 HAS_SCHEMA_ARG=true
999 break
1000 fi
1001 done
1002 # If no schema arg provided, add it
1003 if [ "$HAS_SCHEMA_ARG" = false ]; then
1004 exec cargo run -p rohas-cli -- dev --schema "$REL_SCHEMA_PATH" "$@"
1005 else
1006 exec cargo run -p rohas-cli -- dev "$@"
1007 fi
1008else
1009 # Not in workspace - try installed binary or show helpful error
1010 if command -v rohas >/dev/null 2>&1; then
1011 cd "$SCRIPT_DIR"
1012 exec rohas dev "$@"
1013 else
1014 echo "Error: Could not find Rohas workspace root and rohas CLI is not installed"
1015 echo ""
1016 echo "For Rohas developers: Run this script from within the rohas workspace"
1017 echo "For end users: Install rohas CLI first:"
1018 echo " cargo install --path <path-to-rohas>/crates/rohas-cli"
1019 echo " Then run: rohas dev --workbench"
1020 exit 1
1021 fi
1022fi
1023"#;
1024
1025 let dev_script_path = project_root.join("dev.sh");
1026 fs::write(&dev_script_path, dev_script)?;
1027
1028 #[cfg(unix)]
1030 {
1031 use std::os::unix::fs::PermissionsExt;
1032 let mut perms = fs::metadata(&dev_script_path)?.permissions();
1033 perms.set_mode(0o755);
1034 fs::set_permissions(&dev_script_path, perms)?;
1035 }
1036
1037 let makefile_content = r#"# Makefile for Rohas developers working in examples
1038# End users: Install rohas CLI and use "rohas dev --workbench" directly
1039
1040.PHONY: dev dev-watch codegen check build validate
1041
1042# Run development server (for Rohas developers - finds workspace automatically)
1043# Usage: make dev ARGS="--workbench"
1044dev:
1045 @./dev.sh $(ARGS)
1046
1047# Run development server with workbench
1048dev-watch:
1049 @./dev.sh --workbench
1050
1051# Generate code from schema (for Rohas developers)
1052codegen:
1053 @SCRIPT_DIR=$$(pwd); \
1054 WORKSPACE_ROOT=$$SCRIPT_DIR; \
1055 for i in {1..10}; do \
1056 if [ -f "$$WORKSPACE_ROOT/Cargo.toml" ] && grep -q "^\[workspace\]" "$$WORKSPACE_ROOT/Cargo.toml" 2>/dev/null && [ -d "$$WORKSPACE_ROOT/crates/rohas-cli" ]; then \
1057 break; \
1058 fi; \
1059 WORKSPACE_ROOT=$$(dirname "$$WORKSPACE_ROOT"); \
1060 done; \
1061 cd "$$WORKSPACE_ROOT" && cargo run -p rohas-cli -- codegen --schema "$$SCRIPT_DIR/schema" --output "$$SCRIPT_DIR/src" --lang rust
1062
1063# Check Rust code
1064check:
1065 @CARGO_TARGET_DIR=../../target cargo check
1066
1067# Build Rust project
1068build:
1069 @CARGO_TARGET_DIR=../../target cargo build --release
1070
1071# Validate schema (for Rohas developers)
1072validate:
1073 @SCRIPT_DIR=$$(pwd); \
1074 WORKSPACE_ROOT=$$SCRIPT_DIR; \
1075 for i in {1..10}; do \
1076 if [ -f "$$WORKSPACE_ROOT/Cargo.toml" ] && grep -q "^\[workspace\]" "$$WORKSPACE_ROOT/Cargo.toml" 2>/dev/null && [ -d "$$WORKSPACE_ROOT/crates/rohas-cli" ]; then \
1077 break; \
1078 fi; \
1079 WORKSPACE_ROOT=$$(dirname "$$WORKSPACE_ROOT"); \
1080 done; \
1081 cd "$$WORKSPACE_ROOT" && cargo run -p rohas-cli -- validate --schema "$$SCRIPT_DIR/schema"
1082"#;
1083
1084 fs::write(project_root.join("Makefile"), makefile_content)?;
1085
1086 Ok(())
1087}
1088