From 9483b7fe96e936107c6db8b03230263fe980a951 Mon Sep 17 00:00:00 2001 From: sophatvathana Date: Fri, 5 Dec 2025 09:53:02 +0700 Subject: [PATCH 1/3] feat(rust): add Rust language support for code generation, including handler registration and runtime execution; update dependencies and configuration for Rust integration --- Cargo.lock | 15 +- crates/rohas-cli/Cargo.toml | 1 + crates/rohas-cli/src/commands/codegen.rs | 2 + crates/rohas-cli/src/commands/dev.rs | 48 +- crates/rohas-codegen/src/config.rs | 37 + crates/rohas-codegen/src/generator.rs | 24 +- crates/rohas-codegen/src/lib.rs | 2 + crates/rohas-codegen/src/rust.rs | 1088 +++++++++ crates/rohas-dev-server/Cargo.toml | 3 + crates/rohas-dev-server/src/lib.rs | 578 ++++- crates/rohas-dev-server/src/rust_compiler.rs | 390 ++++ crates/rohas-engine/src/api.rs | 2 + crates/rohas-engine/src/config.rs | 5 +- crates/rohas-engine/src/engine.rs | 48 +- crates/rohas-engine/src/ws.rs | 6 + crates/rohas-parser/src/ast.rs | 13 + crates/rohas-runtime/src/executor.rs | 43 +- crates/rohas-runtime/src/lib.rs | 5 + crates/rohas-runtime/src/rust_runtime.rs | 227 ++ examples/hello-world/pyproject.toml | 2 +- .../src/handlers/api/timeline_test_fast.py | 4 +- examples/rust-example/.editorconfig | 24 + examples/rust-example/.gitignore | 52 + examples/rust-example/Cargo.lock | 1993 +++++++++++++++++ examples/rust-example/Cargo.toml | 21 + examples/rust-example/Makefile | 45 + examples/rust-example/README.md | 66 + examples/rust-example/config/rohas.toml | 33 + examples/rust-example/dev.sh | 61 + examples/rust-example/schema/api/user_api.ro | 19 + .../rust-example/schema/events/user_events.ro | 4 + examples/rust-example/schema/models/user.ro | 6 + .../src/handlers/api/create_user.rs | 21 + .../src/handlers/api/hello_world.rs | 13 + examples/rust-example/src/handlers/api/mod.rs | 4 + .../rust-example/src/handlers/events/mod.rs | 3 + .../src/handlers/events/send_welcome_email.rs | 11 + examples/rust-example/src/handlers/mod.rs | 4 + examples/rust-example/src/lib.rs | 51 + 39 files changed, 4931 insertions(+), 43 deletions(-) create mode 100644 crates/rohas-codegen/src/rust.rs create mode 100644 crates/rohas-dev-server/src/rust_compiler.rs create mode 100644 crates/rohas-runtime/src/rust_runtime.rs create mode 100644 examples/rust-example/.editorconfig create mode 100644 examples/rust-example/.gitignore create mode 100644 examples/rust-example/Cargo.lock create mode 100644 examples/rust-example/Cargo.toml create mode 100644 examples/rust-example/Makefile create mode 100644 examples/rust-example/README.md create mode 100644 examples/rust-example/config/rohas.toml create mode 100755 examples/rust-example/dev.sh create mode 100644 examples/rust-example/schema/api/user_api.ro create mode 100644 examples/rust-example/schema/events/user_events.ro create mode 100644 examples/rust-example/schema/models/user.ro create mode 100644 examples/rust-example/src/handlers/api/create_user.rs create mode 100644 examples/rust-example/src/handlers/api/hello_world.rs create mode 100644 examples/rust-example/src/handlers/api/mod.rs create mode 100644 examples/rust-example/src/handlers/events/mod.rs create mode 100644 examples/rust-example/src/handlers/events/send_welcome_email.rs create mode 100644 examples/rust-example/src/handlers/mod.rs create mode 100644 examples/rust-example/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 3d20bdc..963fcc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1185,7 +1185,7 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading", + "libloading 0.8.9", ] [[package]] @@ -2584,6 +2584,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + [[package]] name = "libm" version = "0.2.15" @@ -3677,11 +3687,14 @@ name = "rohas-dev-server" version = "0.1.0" dependencies = [ "anyhow", + "libloading 0.9.0", "notify", "notify-debouncer-full", "rohas-codegen", "rohas-engine", "rohas-parser", + "rohas-runtime", + "sha2", "tokio", "tokio-test", "tracing", diff --git a/crates/rohas-cli/Cargo.toml b/crates/rohas-cli/Cargo.toml index 5e8bc83..f310b5e 100644 --- a/crates/rohas-cli/Cargo.toml +++ b/crates/rohas-cli/Cargo.toml @@ -4,6 +4,7 @@ version = { workspace = true } edition = { workspace = true } authors = { workspace = true } license = { workspace = true } +default-run = "rohas" [[bin]] name = "rohas" diff --git a/crates/rohas-cli/src/commands/codegen.rs b/crates/rohas-cli/src/commands/codegen.rs index c46e3ce..fffd08a 100644 --- a/crates/rohas-cli/src/commands/codegen.rs +++ b/crates/rohas-cli/src/commands/codegen.rs @@ -15,6 +15,7 @@ fn engine_language_to_codegen_language(lang: EngineLanguage) -> Language { match lang { EngineLanguage::TypeScript => Language::TypeScript, EngineLanguage::Python => Language::Python, + EngineLanguage::Rust => Language::Rust, } } @@ -35,6 +36,7 @@ pub async fn execute( let language = match lang.as_deref() { Some("typescript") | Some("ts") => Language::TypeScript, Some("python") | Some("py") => Language::Python, + Some("rust") | Some("rs") => Language::Rust, None => match &config_path { Some(config_path) => match EngineConfig::from_file(config_path) { Ok(config) => { diff --git a/crates/rohas-cli/src/commands/dev.rs b/crates/rohas-cli/src/commands/dev.rs index 168fae7..6a5abbe 100644 --- a/crates/rohas-cli/src/commands/dev.rs +++ b/crates/rohas-cli/src/commands/dev.rs @@ -14,24 +14,48 @@ pub async fn execute( ) -> Result<()> { info!("Starting development server..."); - let mut config = match EngineConfig::from_project_root() { - Ok(config) => { - info!("Loaded configuration from config/rohas.toml"); - config - } - Err(e) => { - info!("Using default configuration ({})", e); - EngineConfig::default() - } - }; - config.project_root = std::env::current_dir()?; let actual_path = if !schema_path.exists() && schema_path.ends_with("index.ro") { schema_path .parent() .map(|p| p.to_path_buf()) .unwrap_or(schema_path) } else { - schema_path + schema_path.clone() + }; + + let project_root = if actual_path.file_name() + .and_then(|s| s.to_str()) + .map(|s| s == "schema") + .unwrap_or(false) + { + actual_path + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| std::env::current_dir().unwrap_or_default()) + } else { + actual_path.clone() + }; + + let config_path = project_root.join("config").join("rohas.toml"); + let mut config = if config_path.exists() { + match EngineConfig::from_file(&config_path) { + Ok(mut cfg) => { + cfg.project_root = project_root.clone(); + info!("Loaded configuration from {}", config_path.display()); + cfg + } + Err(e) => { + info!("Failed to load config from {}: {}. Using defaults.", config_path.display(), e); + let mut cfg = EngineConfig::default(); + cfg.project_root = project_root.clone(); + cfg + } + } + } else { + info!("Config file not found: {}. Using default configuration.", config_path.display()); + let mut cfg = EngineConfig::default(); + cfg.project_root = project_root.clone(); + cfg }; let dev_server = DevServer::new(actual_path, config.clone(), watch); diff --git a/crates/rohas-codegen/src/config.rs b/crates/rohas-codegen/src/config.rs index 4f3f73f..7b17bd0 100644 --- a/crates/rohas-codegen/src/config.rs +++ b/crates/rohas-codegen/src/config.rs @@ -145,6 +145,43 @@ target-version = "py39" Ok(()) } +pub fn generate_cargo_toml(_schema: &Schema, output_dir: &Path) -> Result<()> { + let project_root = get_project_root(output_dir); + let project_name = extract_project_name(&project_root); + + let lib_name = project_name.replace('-', "_"); + + let content = format!( + r#"[package] +name = "{}" +version = "0.1.0" +edition = "2021" + +[workspace] + +[lib] +name = "{}" +path = "src/lib.rs" + +[dependencies] +rohas-runtime = {{ path = "../../crates/rohas-runtime" }} +serde = {{ version = "1.0", features = ["derive"] }} +serde_json = "1.0" +tokio = {{ version = "1.0", features = ["full"] }} +chrono = {{ version = "0.4", features = ["serde"] }} +tracing = "0.1" + +[dev-dependencies] +tokio-test = "0.4" +"#, + project_name, + lib_name + ); + + fs::write(project_root.join("Cargo.toml"), content)?; + Ok(()) +} + pub fn generate_gitignore(_schema: &Schema, output_dir: &Path) -> Result<()> { let project_root = get_project_root(output_dir); let content = r#"# Dependencies diff --git a/crates/rohas-codegen/src/generator.rs b/crates/rohas-codegen/src/generator.rs index 2116842..98f6d05 100644 --- a/crates/rohas-codegen/src/generator.rs +++ b/crates/rohas-codegen/src/generator.rs @@ -1,5 +1,5 @@ use crate::error::Result; -use crate::{config, python, typescript, Language}; +use crate::{config, python, rust, typescript, Language}; use rohas_parser::Schema; use std::fs; use std::path::Path; @@ -28,6 +28,7 @@ impl Generator { match self.language { Language::TypeScript => self.generate_typescript(schema, output_dir)?, Language::Python => self.generate_python(schema, output_dir)?, + Language::Rust => self.generate_rust(schema, output_dir)?, } info!("Code generation completed successfully"); @@ -109,4 +110,25 @@ impl Generator { Ok(()) } + + fn generate_rust(&self, schema: &Schema, output_dir: &Path) -> Result<()> { + rust::generate_state(output_dir)?; + rust::generate_models(schema, output_dir)?; + rust::generate_dtos(schema, output_dir)?; + rust::generate_apis(schema, output_dir)?; + rust::generate_events(schema, output_dir)?; + rust::generate_crons(schema, output_dir)?; + rust::generate_websockets(schema, output_dir)?; + rust::generate_middlewares(schema, output_dir)?; + rust::generate_lib_rs(schema, output_dir)?; + + info!("Generating Rust configuration files"); + config::generate_cargo_toml(schema, output_dir)?; + + if rust::is_in_rohas_workspace(output_dir) { + rust::generate_dev_scripts(output_dir)?; + } + + Ok(()) + } } diff --git a/crates/rohas-codegen/src/lib.rs b/crates/rohas-codegen/src/lib.rs index 6e133f4..0d31612 100644 --- a/crates/rohas-codegen/src/lib.rs +++ b/crates/rohas-codegen/src/lib.rs @@ -2,6 +2,7 @@ pub mod config; pub mod error; pub mod generator; pub mod python; +pub mod rust; pub mod templates; pub mod typescript; @@ -15,6 +16,7 @@ use std::path::Path; pub enum Language { TypeScript, Python, + Rust, } pub fn generate(schema: &Schema, output_dir: &Path, lang: Language) -> Result<()> { diff --git a/crates/rohas-codegen/src/rust.rs b/crates/rohas-codegen/src/rust.rs new file mode 100644 index 0000000..b276c83 --- /dev/null +++ b/crates/rohas-codegen/src/rust.rs @@ -0,0 +1,1088 @@ +use crate::error::Result; +use crate::templates; +use rohas_parser::{Api, Event, FieldType, Model, Schema, WebSocket}; +use std::fs; +use std::path::Path; + +pub fn generate_models(schema: &Schema, output_dir: &Path) -> Result<()> { + let models_dir = output_dir.join("generated/models"); + + for model in &schema.models { + let content = generate_model_content(model); + let file_name = format!("{}.rs", templates::to_snake_case(&model.name)); + fs::write(models_dir.join(file_name), content)?; + } + + let mut mod_content = String::new(); + mod_content.push_str("// Auto-generated module declarations\n"); + for model in &schema.models { + let mod_name = templates::to_snake_case(&model.name); + mod_content.push_str(&format!("pub mod {};\n", mod_name)); + mod_content.push_str(&format!("pub use {}::{};\n", mod_name, model.name)); + } + fs::write(models_dir.join("mod.rs"), mod_content)?; + + Ok(()) +} + +fn generate_model_content(model: &Model) -> String { + let mut content = String::new(); + + content.push_str("use serde::{Deserialize, Serialize};\n\n"); + content.push_str(&format!("#[derive(Debug, Clone, Serialize, Deserialize)]\n")); + content.push_str(&format!("pub struct {}\n", model.name)); + content.push_str("{\n"); + + for field in &model.fields { + let rust_type = field.field_type.to_rust(); + let type_hint = if field.optional { + format!("Option<{}>", rust_type) + } else { + rust_type + }; + + let field_name = &field.name; + content.push_str(&format!(" pub {}: {},\n", field_name, type_hint)); + } + + if model.fields.is_empty() { + content.push_str(" // No fields\n"); + } + + content.push_str("}\n"); + + content +} + +pub fn generate_dtos(schema: &Schema, output_dir: &Path) -> Result<()> { + let dto_dir = output_dir.join("generated/dto"); + + for input in &schema.inputs { + let content = generate_model_content(&rohas_parser::Model { + name: input.name.clone(), + fields: input.fields.clone(), + attributes: vec![], + }); + let file_name = format!("{}.rs", templates::to_snake_case(&input.name)); + fs::write(dto_dir.join(file_name), content)?; + } + + let mut mod_content = String::new(); + mod_content.push_str("// Auto-generated module declarations\n"); + for input in &schema.inputs { + let mod_name = templates::to_snake_case(&input.name); + mod_content.push_str(&format!("pub mod {};\n", mod_name)); + mod_content.push_str(&format!("pub use {}::{};\n", mod_name, input.name)); + } + fs::write(dto_dir.join("mod.rs"), mod_content)?; + + Ok(()) +} + +pub fn generate_apis(schema: &Schema, output_dir: &Path) -> Result<()> { + let api_dir = output_dir.join("generated/api"); + + for api in &schema.apis { + let content = generate_api_content(api); + let file_name = format!("{}.rs", templates::to_snake_case(&api.name)); + fs::write(api_dir.join(file_name), content)?; + } + + let mut mod_content = String::new(); + mod_content.push_str("// Auto-generated module declarations\n"); + for api in &schema.apis { + let mod_name = templates::to_snake_case(&api.name); + mod_content.push_str(&format!("pub mod {};\n", mod_name)); + mod_content.push_str(&format!("pub use {}::{{ {}Request, {}Response }};\n", mod_name, api.name, api.name)); + } + fs::write(api_dir.join("mod.rs"), mod_content)?; + + let handlers_dir = output_dir.join("handlers/api"); + for api in &schema.apis { + let file_name = format!("{}.rs", templates::to_snake_case(&api.name)); + let handler_path = handlers_dir.join(&file_name); + + if !handler_path.exists() { + let content = generate_api_handler_stub(api); + fs::write(handler_path, content)?; + } + } + + Ok(()) +} + +fn generate_api_content(api: &Api) -> String { + let mut content = String::new(); + + content.push_str("use serde::{Deserialize, Serialize};\n"); + + if let Some(body_type) = &api.body { + let body_type_snake = templates::to_snake_case(body_type); + if body_type.ends_with("Input") { + content.push_str(&format!("use super::super::dto::{}::{};\n", body_type_snake, body_type)); + } else { + content.push_str(&format!("use super::super::models::{}::{};\n", body_type_snake, body_type)); + } + } + + let response_field_type = rohas_parser::FieldType::from_str(&api.response); + let is_custom_response = matches!(response_field_type, rohas_parser::FieldType::Custom(_)); + if is_custom_response { + let response_type_snake = templates::to_snake_case(&api.response); + content.push_str(&format!("use super::super::models::{}::{};\n", response_type_snake, api.response)); + } + content.push_str("\n"); + + if let Some(body_type) = &api.body { + content.push_str(&format!( + "pub type {}Request = {};\n\n", + api.name, body_type + )); + } else { + content.push_str(&format!( + "#[derive(Debug, Clone, Serialize, Deserialize)]\n" + )); + content.push_str(&format!("pub struct {}Request\n", api.name)); + content.push_str("{\n"); + content.push_str(" // No body fields\n"); + content.push_str("}\n\n"); + } + + let response_rust_type = response_field_type.to_rust(); + content.push_str(&format!( + "pub type {}Response = {};\n", + api.name, response_rust_type + )); + + content +} + +fn generate_api_handler_stub(api: &Api) -> String { + let mut content = String::new(); + + let request_type = format!("{}Request", api.name); + let response_type = format!("{}Response", api.name); + let handler_name = format!("handle_{}", templates::to_snake_case(&api.name)); + let module_name = templates::to_snake_case(&api.name); + + content.push_str(&format!( + "use crate::generated::api::{}::{{ {}, {} }};\n", + module_name, request_type, response_type + )); + content.push_str("use crate::generated::state::State;\n"); + content.push_str("use rohas_runtime::{HandlerContext, HandlerResult, Result};\n\n"); + + content.push_str(&format!( + "/// Rust handler for {} API.\n", + api.name + )); + content.push_str(&format!( + "pub async fn {}(\n", + handler_name + )); + content.push_str(&format!(" req: {},\n", request_type)); + content.push_str(" state: &mut State,\n"); + content.push_str(&format!(") -> Result<{}> {{\n", response_type)); + content.push_str(" // TODO: Implement handler logic\n"); + content.push_str(" // For auto-triggers (defined in schema triggers): use state.set_payload(\"EventName\", value)\n"); + content.push_str(" // For manual triggers: use state.trigger_event(\"EventName\", value)\n"); + content.push_str(" // Use state.logger for structured logging\n"); + content.push_str(&format!( + " Err(rohas_runtime::RuntimeError::ExecutionFailed(\"Handler not implemented\".into()))\n" + )); + content.push_str("}\n"); + + content +} + +pub fn generate_events(schema: &Schema, output_dir: &Path) -> Result<()> { + let events_dir = output_dir.join("generated/events"); + + for event in &schema.events { + let content = generate_event_content(event); + let file_name = format!("{}.rs", templates::to_snake_case(&event.name)); + fs::write(events_dir.join(file_name), content)?; + } + + let mut mod_content = String::new(); + mod_content.push_str("// Auto-generated module declarations\n"); + for event in &schema.events { + let mod_name = templates::to_snake_case(&event.name); + mod_content.push_str(&format!("pub mod {};\n", mod_name)); + mod_content.push_str(&format!("pub use {}::{};\n", mod_name, event.name)); + } + fs::write(events_dir.join("mod.rs"), mod_content)?; + + let handlers_dir = output_dir.join("handlers/events"); + for event in &schema.events { + for handler in &event.handlers { + let file_name = format!("{}.rs", handler); + let handler_path = handlers_dir.join(&file_name); + + if !handler_path.exists() { + let content = generate_event_handler_stub(event, handler); + fs::write(handler_path, content)?; + } + } + } + + Ok(()) +} + +fn generate_event_content(event: &Event) -> String { + let mut content = String::new(); + + content.push_str("use serde::{Deserialize, Serialize};\n"); + content.push_str("use chrono::{DateTime, Utc};\n\n"); + + let payload_field_type = FieldType::from_str(&event.payload); + let payload_rust_type = payload_field_type.to_rust(); + + let is_custom_type = matches!(payload_field_type, FieldType::Custom(_)); + if is_custom_type { + let model_module = templates::to_snake_case(&event.payload); + content.push_str(&format!( + "use crate::generated::models::{}::{};\n", + model_module, event.payload + )); + } + + content.push_str(&format!("#[derive(Debug, Clone, Serialize, Deserialize)]\n")); + content.push_str(&format!("pub struct {}\n", event.name)); + content.push_str("{\n"); + content.push_str(&format!(" pub payload: {},\n", payload_rust_type)); + content.push_str(" pub timestamp: DateTime,\n"); + content.push_str("}\n"); + + content +} + +fn generate_event_handler_stub(event: &Event, handler_name: &str) -> String { + let mut content = String::new(); + + let event_module = templates::to_snake_case(&event.name); + + content.push_str(&format!( + "use crate::generated::events::{}::{};\n", + event_module, event.name + )); + content.push_str("use rohas_runtime::{HandlerContext, HandlerResult, Result};\n\n"); + + content.push_str(&format!( + "/// High-performance Rust event handler.\n" + )); + content.push_str(&format!( + "pub async fn {}(\n", + handler_name + )); + content.push_str(&format!(" event: {},\n", event.name)); + content.push_str(") -> Result {\n"); + content.push_str(" // TODO: Implement event handler\n"); + content.push_str(&format!( + " tracing::info!(\"Handling event: {{:?}}\", event);\n" + )); + content.push_str(" Ok(HandlerResult::success(serde_json::json!({}), 0))\n"); + content.push_str("}\n"); + + content +} + +pub fn generate_crons(schema: &Schema, output_dir: &Path) -> Result<()> { + let handlers_dir = output_dir.join("handlers/cron"); + + for cron in &schema.crons { + let file_name = format!("{}.rs", templates::to_snake_case(&cron.name)); + let handler_path = handlers_dir.join(&file_name); + + if !handler_path.exists() { + let content = generate_cron_handler_stub(cron); + fs::write(handler_path, content)?; + } + } + + Ok(()) +} + +fn generate_cron_handler_stub(cron: &rohas_parser::Cron) -> String { + let mut content = String::new(); + + let handler_name = format!("handle_{}", templates::to_snake_case(&cron.name)); + + content.push_str("use rohas_runtime::{HandlerContext, HandlerResult, Result};\n"); + content.push_str("use crate::generated::state::State;\n\n"); + + content.push_str(&format!( + "/// High-performance Rust cron handler.\n" + )); + content.push_str(&format!( + "pub async fn {}(\n", + handler_name + )); + content.push_str(" state: &mut State,\n"); + content.push_str(") -> Result {\n"); + content.push_str(" // TODO: Implement cron handler\n"); + content.push_str(&format!( + " tracing::info!(\"Executing cron: {}\");\n", + cron.name + )); + content.push_str(" Ok(HandlerResult::success(serde_json::json!({}), 0))\n"); + content.push_str("}\n"); + + content +} + +pub fn generate_websockets(schema: &Schema, output_dir: &Path) -> Result<()> { + let ws_dir = output_dir.join("generated/websockets"); + + for ws in &schema.websockets { + let content = generate_websocket_content(ws); + let file_name = format!("{}.rs", templates::to_snake_case(&ws.name)); + fs::write(ws_dir.join(file_name), content)?; + } + + let mut mod_content = String::new(); + mod_content.push_str("// Auto-generated module declarations\n"); + for ws in &schema.websockets { + let mod_name = templates::to_snake_case(&ws.name); + mod_content.push_str(&format!("pub mod {};\n", mod_name)); + mod_content.push_str(&format!("pub use {}::{{ {}Connection", mod_name, ws.name)); + if ws.message.is_some() { + mod_content.push_str(&format!(", {}Message", ws.name)); + } + mod_content.push_str(" };\n"); + } + fs::write(ws_dir.join("mod.rs"), mod_content)?; + + let handlers_dir = output_dir.join("handlers/websockets"); + for ws in &schema.websockets { + for handler in &ws.on_connect { + let file_name = format!("{}.rs", handler); + let handler_path = handlers_dir.join(&file_name); + if !handler_path.exists() { + let content = generate_websocket_handler_stub(ws, handler, "connect"); + fs::write(handler_path, content)?; + } + } + for handler in &ws.on_message { + let file_name = format!("{}.rs", handler); + let handler_path = handlers_dir.join(&file_name); + if !handler_path.exists() { + let content = generate_websocket_handler_stub(ws, handler, "message"); + fs::write(handler_path, content)?; + } + } + for handler in &ws.on_disconnect { + let file_name = format!("{}.rs", handler); + let handler_path = handlers_dir.join(&file_name); + if !handler_path.exists() { + let content = generate_websocket_handler_stub(ws, handler, "disconnect"); + fs::write(handler_path, content)?; + } + } + } + + Ok(()) +} + +fn generate_websocket_content(ws: &WebSocket) -> String { + let mut content = String::new(); + + content.push_str("use serde::{Deserialize, Serialize};\n\n"); + + if let Some(message_type) = &ws.message { + let rust_type = FieldType::from_str(message_type).to_rust(); + + content.push_str(&format!( + "pub type {}Message = {};\n\n", + ws.name, rust_type + )); + } + + content.push_str(&format!( + "#[derive(Debug, Clone, Serialize, Deserialize)]\n" + )); + content.push_str(&format!("pub struct {}Connection\n", ws.name)); + content.push_str("{\n"); + content.push_str(" // Connection metadata\n"); + content.push_str("}\n"); + + content +} + +fn generate_websocket_handler_stub(ws: &WebSocket, handler_name: &str, event_type: &str) -> String { + let mut content = String::new(); + + let ws_module = templates::to_snake_case(&ws.name); + + content.push_str(&format!( + "use crate::generated::websockets::{}::{}Connection;\n", + ws_module, ws.name + )); + + if ws.message.is_some() { + content.push_str(&format!( + "use crate::generated::websockets::{}::{}Message;\n", + ws_module, ws.name + )); + } + + content.push_str("use rohas_runtime::{HandlerContext, HandlerResult, Result};\n"); + content.push_str("use crate::generated::state::State;\n\n"); + + content.push_str(&format!( + "/// Rust WebSocket {} handler.\n", + event_type + )); + content.push_str(&format!("pub async fn {}(\n", handler_name)); + + if event_type == "message" { + if let Some(_) = &ws.message { + content.push_str(&format!(" message: {}Message,\n", ws.name)); + } + content.push_str(&format!(" connection: {}Connection,\n", ws.name)); + content.push_str(" state: &mut State,\n"); + } else { + content.push_str(&format!(" connection: {}Connection,\n", ws.name)); + if event_type == "connect" { + content.push_str(" state: &mut State,\n"); + } + } + + content.push_str(") -> Result {\n"); + content.push_str(&format!( + " tracing::info!(\"WebSocket {} handler: {{:?}}\", connection);\n", + event_type + )); + content.push_str(" Ok(HandlerResult::success(serde_json::json!({}), 0))\n"); + content.push_str("}\n"); + + content +} + +pub fn generate_middlewares(schema: &Schema, output_dir: &Path) -> Result<()> { + let mut middleware_names = std::collections::HashSet::new(); + + for api in &schema.apis { + for mw in &api.middlewares { + middleware_names.insert(mw.clone()); + } + } + + for ws in &schema.websockets { + for mw in &ws.middlewares { + middleware_names.insert(mw.clone()); + } + } + + let handlers_dir = output_dir.join("handlers/middlewares"); + for mw_name in middleware_names { + let file_name = format!("{}.rs", templates::to_snake_case(&mw_name)); + let handler_path = handlers_dir.join(&file_name); + + if !handler_path.exists() { + let content = generate_middleware_stub(&mw_name); + fs::write(handler_path, content)?; + } + } + + Ok(()) +} + +fn generate_middleware_stub(mw_name: &str) -> String { + let mut content = String::new(); + + let handler_name = format!("{}_middleware", templates::to_snake_case(mw_name)); + + content.push_str("use rohas_runtime::{HandlerContext, HandlerResult, Result};\n"); + content.push_str("use crate::generated::state::State;\n\n"); + + content.push_str(&format!( + "/// High-performance Rust middleware.\n" + )); + content.push_str(&format!("pub async fn {}(\n", handler_name)); + content.push_str(" ctx: HandlerContext,\n"); + content.push_str(" state: &mut State,\n"); + content.push_str(") -> Result {\n"); + content.push_str(" // TODO: Implement middleware logic\n"); + content.push_str(" // Return Ok to continue, Err to abort\n"); + content.push_str(&format!( + " tracing::info!(\"Middleware {} executed\");\n", + mw_name + )); + content.push_str(" Ok(HandlerResult::success(serde_json::json!({}), 0))\n"); + content.push_str("}\n"); + + content +} + +pub fn generate_state(output_dir: &Path) -> Result<()> { + let generated_dir = output_dir.join("generated"); + let content = r#"use serde_json::Value; +use std::collections::HashMap; +use tracing::{error, warn, info, debug, trace}; + +/// State struct for Rust handlers. +#[derive(Debug, Clone)] +pub struct State { + handler_name: String, + triggers: Vec, + auto_trigger_payloads: HashMap, +} + +#[derive(Debug, Clone)] +pub struct TriggeredEvent { + pub event_name: String, + pub payload: Value, +} + +impl State { + /// Create a new State instance. + pub fn new(handler_name: impl Into) -> Self { + Self { + handler_name: handler_name.into(), + triggers: Vec::new(), + auto_trigger_payloads: HashMap::new(), + } + } + + /// Manually trigger an event (for events NOT in schema triggers). + pub fn trigger_event(&mut self, event_name: impl Into, payload: Value) { + self.triggers.push(TriggeredEvent { + event_name: event_name.into(), + payload, + }); + } + + /// Set payload for an auto-triggered event (for events IN schema triggers). + pub fn set_payload(&mut self, event_name: impl Into, payload: Value) { + self.auto_trigger_payloads.insert(event_name.into(), payload); + } + + /// Get all manually triggered events (internal use). + pub fn get_triggers(&self) -> &[TriggeredEvent] { + &self.triggers + } + + /// Get all auto-trigger payloads (internal use). + pub fn get_all_auto_trigger_payloads(&self) -> &HashMap { + &self.auto_trigger_payloads + } + + /// Get a logger instance for this handler. + pub fn logger(&self) -> Logger { + Logger::new(&self.handler_name) + } +} + +/// Structured logger for handlers. +pub struct Logger { + handler_name: String, +} + +impl Logger { + pub fn new(handler_name: impl Into) -> Self { + Self { + handler_name: handler_name.into(), + } + } + + pub fn info(&self, message: &str) { + info!(handler = %self.handler_name, %message); + } + + pub fn error(&self, message: &str) { + error!(handler = %self.handler_name, %message); + } + + pub fn warn(&self, message: &str) { + warn!(handler = %self.handler_name, %message); + } + + pub fn debug(&self, message: &str) { + debug!(handler = %self.handler_name, %message); + } + + pub fn trace(&self, message: &str) { + trace!(handler = %self.handler_name, %message); + } +} +"#; + + fs::write(generated_dir.join("state.rs"), content)?; + Ok(()) +} + +/// Generate lib.rs for the generated crate. +pub fn generate_lib_rs(schema: &Schema, output_dir: &Path) -> Result<()> { + let generated_dir = output_dir.join("generated"); + + let mut content = String::new(); + content.push_str("// Auto-generated Rust code from Rohas schema\n"); + content.push_str("// DO NOT EDIT MANUALLY\n\n"); + + // Generate module declarations + content.push_str("pub mod state;\n"); + content.push_str("pub mod models;\n"); + content.push_str("pub mod dto;\n"); + content.push_str("pub mod api;\n"); + content.push_str("pub mod events;\n"); + content.push_str("pub mod websockets;\n"); + content.push_str("pub mod handlers;\n\n"); + + // Re-export commonly used types + content.push_str("pub use state::State;\n"); + content.push_str("pub use handlers::register_all_handlers;\n"); + content.push_str("pub use handlers::set_runtime;\n\n"); + + fs::write(generated_dir.join("lib.rs"), content)?; + + // Generate handlers registration module + generate_handlers_registration(schema, output_dir)?; + + + // Also generate the main src/lib.rs that sets up the module structure + let mut main_lib_content = String::new(); + main_lib_content.push_str("// Main library entry point for Rohas Rust application\n"); + main_lib_content.push_str("// This file sets up the module structure\n\n"); + main_lib_content.push_str("#[path = \"generated/lib.rs\"]\n"); + main_lib_content.push_str("pub mod generated;\n\n"); + main_lib_content.push_str("// Re-export generated types for convenience\n"); + main_lib_content.push_str("pub use generated::*;\n\n"); + + // Generate handlers module declarations + let handlers_dir = output_dir.join("handlers"); + if handlers_dir.join("api").exists() || handlers_dir.join("events").exists() { + main_lib_content.push_str("pub mod handlers;\n\n"); + } + + // Add initialization function that can be called to register handlers + main_lib_content.push_str("/// Initialize and register all handlers with the Rust runtime.\n"); + main_lib_content.push_str("/// This function should be called during engine startup.\n"); + main_lib_content.push_str("/// It will automatically register all handlers using the global registry.\n"); + main_lib_content.push_str("pub async fn init_handlers(runtime: std::sync::Arc) -> rohas_runtime::Result<()> {\n"); + main_lib_content.push_str(" generated::register_all_handlers(runtime).await\n"); + main_lib_content.push_str("}\n\n"); + + // Add a C-compatible FFI function that can be called from the engine + // This allows the engine to automatically register handlers + main_lib_content.push_str("/// C-compatible FFI function for automatic handler registration.\n"); + main_lib_content.push_str("/// This is called automatically by the engine.\n"); + main_lib_content.push_str("/// Returns 0 on success, non-zero on error.\n"); + main_lib_content.push_str("#[no_mangle]\n"); + main_lib_content.push_str("pub extern \"C\" fn rohas_set_runtime(runtime_ptr: *mut std::ffi::c_void) -> i32 {\n"); + main_lib_content.push_str(" use std::sync::Arc;\n"); + main_lib_content.push_str(" \n"); + main_lib_content.push_str(" if runtime_ptr.is_null() {\n"); + main_lib_content.push_str(" return 1; // Error: null pointer\n"); + main_lib_content.push_str(" }\n"); + main_lib_content.push_str(" \n"); + main_lib_content.push_str(" // Safety: The engine passes a valid Arc pointer that was created with Arc::into_raw.\n"); + main_lib_content.push_str(" // We reconstruct the Arc temporarily to clone it, then forget it so the engine retains ownership.\n"); + main_lib_content.push_str(" unsafe {\n"); + main_lib_content.push_str(" // Convert the raw pointer back to Arc\n"); + main_lib_content.push_str(" // The engine created this with Arc::into_raw, so we reconstruct it temporarily\n"); + main_lib_content.push_str(" let runtime: Arc = Arc::from_raw(runtime_ptr as *const rohas_runtime::RustRuntime);\n"); + main_lib_content.push_str(" \n"); + main_lib_content.push_str(" // Clone the Arc - this increments the reference count\n"); + main_lib_content.push_str(" let runtime_clone = runtime.clone();\n"); + main_lib_content.push_str(" \n"); + main_lib_content.push_str(" // Forget the reconstructed Arc - we don't want to drop it here since the engine still owns it\n"); + main_lib_content.push_str(" // The engine will manage the original Arc's lifetime\n"); + main_lib_content.push_str(" std::mem::forget(runtime);\n"); + main_lib_content.push_str(" \n"); + main_lib_content.push_str(" // Call the generated set_runtime function which will register all handlers\n"); + main_lib_content.push_str(" // This will store the cloned Arc in a OnceLock and register handlers synchronously\n"); + main_lib_content.push_str(" // Note: If registration fails, set_runtime will panic (via .expect())\n"); + main_lib_content.push_str(" generated::set_runtime(runtime_clone);\n"); + main_lib_content.push_str(" \n"); + main_lib_content.push_str(" 0 // Success\n"); + main_lib_content.push_str(" }\n"); + main_lib_content.push_str("}\n"); + + fs::write(output_dir.join("lib.rs"), main_lib_content)?; + + // Generate handlers/mod.rs if handlers exist + if handlers_dir.join("api").exists() || handlers_dir.join("events").exists() { + generate_handlers_mod(schema, output_dir)?; + } + + Ok(()) +} + +fn generate_handlers_mod(schema: &Schema, output_dir: &Path) -> Result<()> { + let handlers_dir = output_dir.join("handlers"); + let mut content = String::new(); + + content.push_str("// Handler module declarations\n\n"); + + if handlers_dir.join("api").exists() { + content.push_str("pub mod api;\n"); + } + + if handlers_dir.join("events").exists() { + content.push_str("pub mod events;\n"); + } + + fs::write(handlers_dir.join("mod.rs"), content)?; + + if handlers_dir.join("api").exists() { + let mut api_mod = String::new(); + api_mod.push_str("// API handler modules\n\n"); + + for api in &schema.apis { + let handler_name = templates::to_snake_case(&api.name); + let handler_file = handlers_dir.join("api").join(format!("{}.rs", handler_name)); + if handler_file.exists() { + api_mod.push_str(&format!("pub mod {};\n", handler_name)); + } + } + + fs::write(handlers_dir.join("api").join("mod.rs"), api_mod)?; + } + + if handlers_dir.join("events").exists() { + let mut events_mod = String::new(); + events_mod.push_str("// Event handler modules\n\n"); + + for event in &schema.events { + for handler in &event.handlers { + let handler_file = handlers_dir.join("events").join(format!("{}.rs", handler)); + if handler_file.exists() { + events_mod.push_str(&format!("pub mod {};\n", handler)); + } + } + } + + fs::write(handlers_dir.join("events").join("mod.rs"), events_mod)?; + } + + Ok(()) +} + +fn generate_handlers_registration(schema: &Schema, output_dir: &Path) -> Result<()> { + let generated_dir = output_dir.join("generated"); + let handlers_dir = output_dir.join("handlers"); + + let mut content = String::new(); + content.push_str("// Auto-generated handler registration\n"); + content.push_str("// DO NOT EDIT MANUALLY\n\n"); + + content.push_str("use rohas_runtime::{RustRuntime, HandlerContext, HandlerResult, Result};\n"); + content.push_str("use std::sync::Arc;\n"); + content.push_str("use std::sync::OnceLock;\n\n"); + + content.push_str("// Global registry for automatic handler registration\n"); + content.push_str("static RUNTIME_REGISTRY: OnceLock> = OnceLock::new();\n\n"); + content.push_str("/// Set the runtime for automatic handler registration.\n"); + content.push_str("/// This is called automatically by the engine.\n"); + content.push_str("/// This function is public so it can be called from the engine.\n"); + content.push_str("/// Note: Each dylib has its own OnceLock, so this can be called fresh on each reload.\n"); + content.push_str("pub fn set_runtime(runtime: Arc) {\n"); + content.push_str(" // Set the runtime (this will only succeed once per dylib load, which is what we want)\n"); + content.push_str(" let _ = RUNTIME_REGISTRY.set(runtime);\n"); + content.push_str(" // Always trigger registration (important for hot reload)\n"); + content.push_str(" register_all_handlers_internal().expect(\"Failed to register handlers\");\n"); + content.push_str("}\n\n"); + + let mut has_handlers = false; + + for api in &schema.apis { + let handler_name = templates::to_snake_case(&api.name); + let handler_file = handlers_dir.join("api").join(format!("{}.rs", handler_name)); + if handler_file.exists() { + has_handlers = true; + break; + } + } + + if !has_handlers { + for event in &schema.events { + for handler in &event.handlers { + let handler_file = handlers_dir.join("events").join(format!("{}.rs", handler)); + if handler_file.exists() { + has_handlers = true; + break; + } + } + if has_handlers { + break; + } + } + } + + if !has_handlers { + content.push_str("/// Register all handlers with the Rust runtime.\n"); + content.push_str("/// No handlers found - implement handlers in src/handlers/ to register them.\n"); + content.push_str("pub async fn register_all_handlers(_runtime: Arc) -> Result<()> {\n"); + content.push_str(" Ok(())\n"); + content.push_str("}\n\n"); + content.push_str("fn register_all_handlers_internal() -> Result<()> {\n"); + content.push_str(" Ok(())\n"); + content.push_str("}\n"); + fs::write(generated_dir.join("handlers.rs"), content)?; + return Ok(()); + } + + content.push_str("// Import handler functions\n"); + + for api in &schema.apis { + let handler_name = templates::to_snake_case(&api.name); + let handler_file = handlers_dir.join("api").join(format!("{}.rs", handler_name)); + + if handler_file.exists() { + content.push_str(&format!( + "use crate::handlers::api::{}::handle_{};\n", + handler_name, handler_name + )); + } + } + + for event in &schema.events { + for handler in &event.handlers { + let handler_file = handlers_dir.join("events").join(format!("{}.rs", handler)); + + if handler_file.exists() { + content.push_str(&format!( + "use crate::handlers::events::{}::{};\n", + handler, handler + )); + } + } + } + + content.push_str("\n"); + content.push_str("/// Register all handlers with the Rust runtime.\n"); + content.push_str("/// This function should be called during engine initialization.\n"); + content.push_str("pub async fn register_all_handlers(runtime: Arc) -> Result<()> {\n"); + content.push_str(" set_runtime(runtime);\n"); + content.push_str(" Ok(())\n"); + content.push_str("}\n\n"); + + content.push_str("/// Internal registration function (synchronous, for static initialization).\n"); + content.push_str("fn register_all_handlers_internal() -> Result<()> {\n"); + content.push_str(" use tracing::info;\n"); + content.push_str(" info!(\"Registering Rust handlers from dylib...\");\n"); + content.push_str(" let runtime = RUNTIME_REGISTRY.get().ok_or_else(|| rohas_runtime::RuntimeError::ExecutionFailed(\"Runtime not set\".into()))?;\n"); + content.push_str(" let rt = tokio::runtime::Runtime::new().map_err(|e| rohas_runtime::RuntimeError::ExecutionFailed(e.to_string()))?;\n"); + content.push_str(" rt.block_on(async {\n"); + + for api in &schema.apis { + let handler_name = templates::to_snake_case(&api.name); + let handler_file = handlers_dir.join("api").join(format!("{}.rs", handler_name)); + + if handler_file.exists() { + content.push_str(&format!( + " // Register API handler: {}\n", + api.name + )); + content.push_str(&format!( + " runtime.register_handler(\n" + )); + content.push_str(&format!( + " \"{}\".to_string(),\n", + handler_name + )); + content.push_str(&format!( + " |ctx: HandlerContext| async move {{\n" + )); + content.push_str(&format!( + " // Parse request from context\n" + )); + content.push_str(&format!( + " let req: crate::generated::api::{}::{}Request = serde_json::from_value(ctx.payload.clone())?;\n", + handler_name, api.name + )); + content.push_str(&format!( + " let mut state = crate::generated::state::State::new(&ctx.handler_name);\n" + )); + content.push_str(&format!( + " let response = handle_{}(req, &mut state).await?;\n", + handler_name + )); + content.push_str(&format!( + " Ok(HandlerResult::success(serde_json::to_value(response)?, 0))\n" + )); + content.push_str(&format!( + " }}\n" + )); + content.push_str(&format!( + " ).await;\n" + )); + content.push_str(&format!( + " info!(\"Registered handler: {}\");\n", + handler_name + )); + } + } + + content.push_str(" Ok::<(), rohas_runtime::RuntimeError>(())\n"); + content.push_str(" })?;\n"); + content.push_str(" Ok(())\n"); + content.push_str("}\n"); + + fs::write(generated_dir.join("handlers.rs"), content)?; + Ok(()) +} + +pub fn is_in_rohas_workspace(output_dir: &Path) -> bool { + let project_root = if output_dir.file_name().and_then(|s| s.to_str()) == Some("src") { + output_dir.parent().unwrap_or(output_dir) + } else { + output_dir + }; + + let path_str = project_root.to_string_lossy(); + if path_str.contains("/examples/") || path_str.contains("\\examples\\") { + return true; + } + + let mut current = project_root; + for _ in 0..5 { + let crates_dir = current.join("crates").join("rohas-cli"); + if crates_dir.exists() { + return true; + } + if let Some(parent) = current.parent() { + current = parent; + } else { + break; + } + } + + false +} + +pub fn generate_dev_scripts(output_dir: &Path) -> Result<()> { + let project_root = if output_dir.file_name().and_then(|s| s.to_str()) == Some("src") { + output_dir.parent().unwrap_or(output_dir).to_path_buf() + } else { + output_dir.to_path_buf() + }; + + let dev_script = r#"#!/bin/bash +# Development helper script for Rohas developers +# For end users: install rohas CLI and run "rohas dev --workbench" directly + +set -e + +# Find the workspace root (look for Cargo.toml with [workspace]) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKSPACE_ROOT="$SCRIPT_DIR" + +# Look for workspace root (go up to 10 levels to handle nested examples) +for i in {1..10}; do + if [ -f "$WORKSPACE_ROOT/Cargo.toml" ]; then + # Check if it's a workspace (has [workspace] and contains crates/rohas-cli) + if grep -q "^\[workspace\]" "$WORKSPACE_ROOT/Cargo.toml" 2>/dev/null && \ + [ -d "$WORKSPACE_ROOT/crates/rohas-cli" ]; then + break + fi + fi + WORKSPACE_ROOT="$(dirname "$WORKSPACE_ROOT")" + # Stop if we've reached the filesystem root + if [ "$WORKSPACE_ROOT" = "/" ] || [ "$WORKSPACE_ROOT" = "$SCRIPT_DIR" ]; then + break + fi +done + +if [ -f "$WORKSPACE_ROOT/Cargo.toml" ] && grep -q "^\[workspace\]" "$WORKSPACE_ROOT/Cargo.toml" 2>/dev/null && \ + [ -d "$WORKSPACE_ROOT/crates/rohas-cli" ]; then + cd "$WORKSPACE_ROOT" + REL_SCHEMA_PATH=$(python3 -c "import os; print(os.path.relpath('$SCRIPT_DIR/schema', '$WORKSPACE_ROOT'))" 2>/dev/null || \ + perl -MFile::Spec -e "print File::Spec->abs2rel('$SCRIPT_DIR/schema', '$WORKSPACE_ROOT')" 2>/dev/null || \ + echo "schema") + # Check if --schema argument is already provided + HAS_SCHEMA_ARG=false + for arg in "$@"; do + if [[ "$arg" == "--schema" ]] || [[ "$arg" == "-s" ]]; then + HAS_SCHEMA_ARG=true + break + fi + done + # If no schema arg provided, add it + if [ "$HAS_SCHEMA_ARG" = false ]; then + exec cargo run -p rohas-cli -- dev --schema "$REL_SCHEMA_PATH" "$@" + else + exec cargo run -p rohas-cli -- dev "$@" + fi +else + # Not in workspace - try installed binary or show helpful error + if command -v rohas >/dev/null 2>&1; then + cd "$SCRIPT_DIR" + exec rohas dev "$@" + else + echo "Error: Could not find Rohas workspace root and rohas CLI is not installed" + echo "" + echo "For Rohas developers: Run this script from within the rohas workspace" + echo "For end users: Install rohas CLI first:" + echo " cargo install --path /crates/rohas-cli" + echo " Then run: rohas dev --workbench" + exit 1 + fi +fi +"#; + + let dev_script_path = project_root.join("dev.sh"); + fs::write(&dev_script_path, dev_script)?; + + // Make it executable (Unix-like systems) + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&dev_script_path)?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(&dev_script_path, perms)?; + } + + let makefile_content = r#"# Makefile for Rohas developers working in examples +# End users: Install rohas CLI and use "rohas dev --workbench" directly + +.PHONY: dev dev-watch codegen check build validate + +# Run development server (for Rohas developers - finds workspace automatically) +# Usage: make dev ARGS="--workbench" +dev: + @./dev.sh $(ARGS) + +# Run development server with workbench +dev-watch: + @./dev.sh --workbench + +# Generate code from schema (for Rohas developers) +codegen: + @SCRIPT_DIR=$$(pwd); \ + WORKSPACE_ROOT=$$SCRIPT_DIR; \ + for i in {1..10}; do \ + if [ -f "$$WORKSPACE_ROOT/Cargo.toml" ] && grep -q "^\[workspace\]" "$$WORKSPACE_ROOT/Cargo.toml" 2>/dev/null && [ -d "$$WORKSPACE_ROOT/crates/rohas-cli" ]; then \ + break; \ + fi; \ + WORKSPACE_ROOT=$$(dirname "$$WORKSPACE_ROOT"); \ + done; \ + cd "$$WORKSPACE_ROOT" && cargo run -p rohas-cli -- codegen --schema "$$SCRIPT_DIR/schema" --output "$$SCRIPT_DIR/src" --lang rust + +# Check Rust code +check: + @CARGO_TARGET_DIR=../../target cargo check + +# Build Rust project +build: + @CARGO_TARGET_DIR=../../target cargo build --release + +# Validate schema (for Rohas developers) +validate: + @SCRIPT_DIR=$$(pwd); \ + WORKSPACE_ROOT=$$SCRIPT_DIR; \ + for i in {1..10}; do \ + if [ -f "$$WORKSPACE_ROOT/Cargo.toml" ] && grep -q "^\[workspace\]" "$$WORKSPACE_ROOT/Cargo.toml" 2>/dev/null && [ -d "$$WORKSPACE_ROOT/crates/rohas-cli" ]; then \ + break; \ + fi; \ + WORKSPACE_ROOT=$$(dirname "$$WORKSPACE_ROOT"); \ + done; \ + cd "$$WORKSPACE_ROOT" && cargo run -p rohas-cli -- validate --schema "$$SCRIPT_DIR/schema" +"#; + + fs::write(project_root.join("Makefile"), makefile_content)?; + + Ok(()) +} + diff --git a/crates/rohas-dev-server/Cargo.toml b/crates/rohas-dev-server/Cargo.toml index b9a6714..e9343f7 100644 --- a/crates/rohas-dev-server/Cargo.toml +++ b/crates/rohas-dev-server/Cargo.toml @@ -9,12 +9,15 @@ license = { workspace = true } rohas-parser = { workspace = true } rohas-engine = { workspace = true } rohas-codegen = { workspace = true } +rohas-runtime = { workspace = true } tokio = { workspace = true } anyhow = { workspace = true } tracing = { workspace = true } notify = { workspace = true } notify-debouncer-full = { workspace = true } +libloading = "0.9.0" +sha2 = "0.10" [dev-dependencies] tokio-test = "0.4" diff --git a/crates/rohas-dev-server/src/lib.rs b/crates/rohas-dev-server/src/lib.rs index b435585..0e80b0b 100644 --- a/crates/rohas-dev-server/src/lib.rs +++ b/crates/rohas-dev-server/src/lib.rs @@ -1,3 +1,4 @@ +mod rust_compiler; mod ts_compiler; use notify::RecursiveMode; @@ -5,6 +6,8 @@ use notify_debouncer_full::{new_debouncer, DebounceEventResult}; use rohas_codegen::{self, Language as CodegenLanguage}; use rohas_engine::{config::Language as EngineLanguage, Engine, EngineConfig}; use rohas_parser::{Parser, Schema}; +use rust_compiler::RustCompiler; +use tracing::debug; use std::fs; use std::path::PathBuf; use std::sync::Arc; @@ -19,6 +22,9 @@ pub struct DevServer { watch: bool, engine: Arc>>, ts_compiler: Arc>>, + rust_compiler: Arc>>, + rust_library: Arc>>, + last_loaded_dylib_hash: Arc>>, } impl DevServer { @@ -29,6 +35,9 @@ impl DevServer { watch, engine: Arc::new(RwLock::new(None)), ts_compiler: Arc::new(RwLock::new(None)), + rust_compiler: Arc::new(RwLock::new(None)), + rust_library: Arc::new(tokio::sync::Mutex::new(None)), + last_loaded_dylib_hash: Arc::new(tokio::sync::Mutex::new(None)), } } @@ -62,6 +71,11 @@ impl DevServer { package_json.exists() && swcrc.exists() } + fn is_rust_project(&self) -> bool { + let project_root = self.get_project_root(); + RustCompiler::is_rust_project(&project_root) + } + pub async fn run(&self) -> anyhow::Result<()> { info!("Starting Rohas development server"); info!(" Schema: {}", self.schema_path.display()); @@ -73,6 +87,11 @@ impl DevServer { self.setup_typescript_compiler().await?; } + if self.is_rust_project() { + info!("Detected Rust project"); + self.setup_rust_compiler().await?; + } + self.reload_engine().await?; if self.watch { @@ -117,6 +136,23 @@ impl DevServer { Ok(()) } + async fn setup_rust_compiler(&self) -> anyhow::Result<()> { + let project_root = self.get_project_root(); + info!( + "Setting up Rust compiler in: {}", + project_root.display() + ); + + let compiler = RustCompiler::new(project_root); + + compiler.compile()?; + + let mut rust_compiler = self.rust_compiler.write().await; + *rust_compiler = Some(compiler); + + Ok(()) + } + async fn reload_engine(&self) -> anyhow::Result<()> { info!("Loading engine..."); @@ -131,7 +167,7 @@ impl DevServer { self.run_codegen(&schema)?; let engine = Engine::from_schema(schema, self.config.clone()).await?; - + let layer = engine.create_tracing_log_layer(); if let Err(e) = rohas_engine::tracing_log::register_tracing_log_layer(layer) { warn!("Failed to register tracing log layer: {}", e); @@ -141,6 +177,15 @@ impl DevServer { engine.initialize().await?; + { + let mut rust_lib = self.rust_library.lock().await; + *rust_lib = None; + } + + if self.is_rust_project() { + self.register_rust_handlers(&engine, true).await?; + } + let mut engine_lock = self.engine.write().await; *engine_lock = Some(engine); @@ -149,6 +194,234 @@ impl DevServer { Ok(()) } + async fn register_rust_handlers(&self, engine: &Engine, should_build: bool) -> anyhow::Result<()> { + let project_root = self.get_project_root(); + let executor = engine.executor(); + let rust_runtime = executor.rust_runtime().clone(); + + info!("Registering Rust handlers via dylib loading..."); + + let cargo_toml = project_root.join("Cargo.toml"); + if !cargo_toml.exists() { + return Ok(()); + } + + let cargo_content = std::fs::read_to_string(&cargo_toml)?; + let needs_dylib_config = !cargo_content.contains("crate-type") || !cargo_content.contains("dylib"); + + if needs_dylib_config { + let updated_content = if cargo_content.contains("[lib]") { + if cargo_content.contains("crate-type") { + cargo_content.replace( + "crate-type = [", + "crate-type = [\"dylib\", " + ) + } else { + cargo_content.replace( + "[lib]", + "[lib]\ncrate-type = [\"dylib\", \"rlib\"]" + ) + } + } else { + format!("{}\n\n[lib]\ncrate-type = [\"dylib\", \"rlib\"]", cargo_content) + }; + std::fs::write(&cargo_toml, updated_content)?; + info!("Updated Cargo.toml to build as dylib"); + } + + let compiler = crate::rust_compiler::RustCompiler::new(project_root.clone()); + if should_build { + info!("Building Rust project as dylib..."); + compiler.build_release().await?; + } else { + info!("Skipping build (already built)..."); + } + + let dylib_path = compiler.get_library_path_for_profile("release")?; + + if !dylib_path.exists() { + let debug_dylib = compiler.get_library_path_for_profile("debug")?; + + if debug_dylib.exists() { + return self.load_and_register_handlers(&debug_dylib, rust_runtime).await; + } else { + warn!("Rust dylib not found at: {} or {}", dylib_path.display(), debug_dylib.display()); + return Ok(()); + } + } + + self.load_and_register_handlers_with_path(&dylib_path, rust_runtime, None).await + } + + async fn load_and_register_handlers_with_path( + &self, + dylib_path: &std::path::Path, + runtime: Arc, + expected_hash: Option<[u8; 32]>, + ) -> anyhow::Result<()> { + if let Some(hash) = expected_hash { + let mut last_hash = self.last_loaded_dylib_hash.lock().await; + let prev_hash = *last_hash; + drop(last_hash); + + let result = self.load_and_register_handlers(dylib_path, runtime).await; + + if result.is_ok() { + let computed_hash: [u8; 32] = { + use sha2::{Sha256, Digest}; + use std::io::Read; + let mut file = std::fs::File::open(dylib_path)?; + let mut hasher = Sha256::new(); + let mut buffer = vec![0u8; 8192]; + loop { + let bytes_read = file.read(&mut buffer)?; + if bytes_read == 0 { + break; + } + hasher.update(&buffer[..bytes_read]); + } + hasher.finalize().into() + }; + + if computed_hash != hash { + error!("ERROR: Computed dylib hash doesn't match expected hash!"); + error!("This indicates the dylib file changed between rebuild and load."); + return Err(anyhow::anyhow!("Dylib hash mismatch")); + } + } + + result + } else { + self.load_and_register_handlers(dylib_path, runtime).await + } + } + + async fn load_and_register_handlers( + &self, + dylib_path: &std::path::Path, + runtime: Arc, + ) -> anyhow::Result<()> { + use libloading::{Library, Symbol}; + use std::ffi::c_void; + use std::sync::Arc; + use sha2::{Sha256, Digest}; + use std::io::Read; + + info!("Loading Rust dylib from: {}", dylib_path.display()); + + let dylib_hash: [u8; 32] = { + let mut file = std::fs::File::open(dylib_path)?; + let mut hasher = Sha256::new(); + let mut buffer = vec![0u8; 8192]; + loop { + let bytes_read = file.read(&mut buffer)?; + if bytes_read == 0 { + break; + } + hasher.update(&buffer[..bytes_read]); + } + hasher.finalize().into() + }; + + let last_hash = { + let last_hash_lock = self.last_loaded_dylib_hash.lock().await; + *last_hash_lock + }; + + if let Some(prev_hash) = last_hash { + if dylib_hash == prev_hash { + error!("ERROR: Dylib hash unchanged! The dylib file is identical to the last loaded version."); + error!("This means either:"); + error!(" 1. The code wasn't actually rebuilt (Cargo didn't detect changes)"); + error!(" 2. macOS is caching the old dylib by path"); + error!(" 3. The build produced identical output"); + error!("Hot reload will NOT work - the old code will still be running!"); + + warn!("Attempting to force reload by waiting longer and using canonical path..."); + tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; + + let canonical_path = dylib_path.canonicalize()?; + info!("Using canonical path for dylib: {}", canonical_path.display()); + + } else { + let hash_prefix = u128::from_be_bytes( + dylib_hash[..16].try_into().unwrap_or([0; 16]) + ); + let prev_hash_prefix = u128::from_be_bytes( + prev_hash[..16].try_into().unwrap_or([0; 16]) + ); + info!("Dylib hash changed: 0x{:032x} -> 0x{:032x} (new dylib detected)", prev_hash_prefix, hash_prefix); + } + } else { + let hash_prefix = u128::from_be_bytes( + dylib_hash[..16].try_into().unwrap_or([0; 16]) + ); + info!("Loading dylib for the first time (hash: 0x{:032x})", hash_prefix); + } + + let handler_count_before_load = runtime.handler_count().await; + let handlers_before_load = runtime.list_handlers().await; + info!("Handler state before loading dylib: count={}, handlers={:?}", handler_count_before_load, handlers_before_load); + + unsafe { + let dylib_metadata = dylib_path.metadata()?; + let dylib_size = dylib_metadata.len(); + let dylib_mtime = dylib_metadata.modified().ok(); + info!("Loading dylib: size={} bytes, mtime={:?}", dylib_size, dylib_mtime); + + let dylib_path_to_load = dylib_path.canonicalize().unwrap_or_else(|_| dylib_path.to_path_buf()); + info!("Loading dylib from canonical path: {}", dylib_path_to_load.display()); + + let lib = Library::new(&dylib_path_to_load)?; + info!("Dylib loaded successfully, registering handlers..."); + + type SetRuntimeFn = unsafe extern "C" fn(*mut c_void) -> i32; + let set_runtime: Symbol = lib.get(b"rohas_set_runtime")?; + + let runtime_ptr = Arc::into_raw(runtime) as *mut c_void; + + let result = set_runtime(runtime_ptr); + + if result == 0 { + let runtime: Arc = Arc::from_raw(runtime_ptr as *const rohas_runtime::RustRuntime); + let runtime_clone = runtime.clone(); + std::mem::forget(runtime); + + let handler_count_after = runtime_clone.handler_count().await; + let handlers_after = runtime_clone.list_handlers().await; + info!("Rust handlers registered successfully: count={}, handlers={:?}", handler_count_after, handlers_after); + + if handler_count_after == 0 { + warn!("No handlers were registered from the dylib!"); + } else if handler_count_after == handler_count_before_load && handlers_after == handlers_before_load { + warn!("Handler count and list unchanged - handlers may not have been re-registered!"); + } else { + info!("Handlers successfully updated: {} -> {}", handler_count_before_load, handler_count_after); + } + + let mut rust_lib = self.rust_library.lock().await; + if rust_lib.is_some() { + warn!("Replacing existing dylib - this should only happen during hot reload"); + } + *rust_lib = Some(lib); + + { + let mut last_hash = self.last_loaded_dylib_hash.lock().await; + *last_hash = Some(dylib_hash); + } + + info!("Rust dylib kept in memory (hash stored for next reload verification)"); + } else { + let _runtime: Arc = Arc::from_raw(runtime_ptr as *const rohas_runtime::RustRuntime); + warn!("rohas_set_runtime returned error code: {}", result); + return Err(anyhow::anyhow!("Failed to register Rust handlers: set_runtime returned {}", result)); + } + + } + + Ok(()) + } + fn schema_dir(&self) -> PathBuf { if self.schema_path.is_dir() { self.schema_path.clone() @@ -165,7 +438,6 @@ impl DevServer { let (tx, mut rx) = tokio::sync::mpsc::channel(100); - // Create debouncer and keep it alive for the entire duration let mut debouncer = new_debouncer( Duration::from_millis(500), None, @@ -179,22 +451,22 @@ impl DevServer { .map(|s| s.to_ascii_lowercase()); if let Some(ext_str) = ext.as_deref() { - // Schema files trigger full engine reload if ext_str == "ro" || ext_str == "roh" { if tx.blocking_send((path.clone(), ext_str.to_string())).is_err() { - error!("File watcher channel full, dropping event for: {}", path.display()); + eprintln!("[File Watcher] Channel full, dropping event for: {}", path.display()); } } - else if ext_str == "ts" || ext_str == "tsx" || ext_str == "py" { + else if ext_str == "ts" || ext_str == "tsx" || ext_str == "py" || ext_str == "rs" { + eprintln!("[File Watcher] Detected {} file change: {}", ext_str, path.display()); if tx.blocking_send((path.clone(), ext_str.to_string())).is_err() { - error!("File watcher channel full, dropping event for: {}", path.display()); + eprintln!("[File Watcher] Channel full, dropping event for: {}", path.display()); } } } } } } - Err(e) => error!("Watch error: {:?}", e), + Err(e) => eprintln!("[File Watcher] Watch error: {:?}", e), }, )?; @@ -212,7 +484,7 @@ impl DevServer { let _debouncer_guard = debouncer; - + let mut server_handle = { let engine = self.engine.clone(); Some(tokio::spawn(async move { @@ -224,7 +496,7 @@ impl DevServer { })) }; - + let reloading = Arc::new(tokio::sync::RwLock::new(false)); // Main event loop: respond to Ctrl+C and file changes. @@ -243,6 +515,8 @@ impl DevServer { break; }; + info!("File change detected: {} (ext: {})", path.display(), ext); + let should_process = { let is_reloading = reloading.read().await; if *is_reloading { @@ -257,8 +531,6 @@ impl DevServer { continue; } - info!("File changed: {}", path.display()); - let ext = ext.to_ascii_lowercase(); if ext == "ro" || ext == "roh" { @@ -273,12 +545,12 @@ impl DevServer { info!("Stopping current HTTP server..."); handle.abort(); tokio::time::sleep(Duration::from_millis(500)).await; - + let _ = tokio::time::timeout(Duration::from_millis(100), handle).await; } let reload_result = self.reload_engine().await; - + { let mut reloading_flag = reloading.write().await; *reloading_flag = false; @@ -291,7 +563,7 @@ impl DevServer { info!("Starting new HTTP server with updated schema..."); let engine = self.engine.clone(); let port = self.config.server.port; - + let new_handle = tokio::spawn(async move { if let Some(eng) = engine.read().await.as_ref() { if let Err(e) = eng.start_server().await { @@ -301,9 +573,9 @@ impl DevServer { error!("Engine not available when starting server"); } }); - + tokio::time::sleep(Duration::from_millis(200)).await; - + if !new_handle.is_finished() { server_handle = Some(new_handle); info!("HTTP server restarted with new schema on port {}", port); @@ -318,7 +590,7 @@ impl DevServer { } } } - + tokio::time::sleep(Duration::from_millis(100)).await; } Err(e) => { @@ -326,10 +598,10 @@ impl DevServer { warn!("Continuing to watch for changes..."); } } - } else if ext == "ts" || ext == "tsx" || ext == "py" { + } else if ext == "ts" || ext == "tsx" { let path_str = path.to_string_lossy(); let is_generated = path_str.contains("/generated/") || path_str.contains("\\generated\\"); - + if is_generated { warn!("Generated file changed - clearing handler cache..."); if let Some(eng) = self.engine.read().await.as_ref() { @@ -352,6 +624,60 @@ impl DevServer { } } } + } else if ext == "py" { + let path_str = path.to_string_lossy(); + let is_generated = path_str.contains("/generated/") || path_str.contains("\\generated\\"); + + if is_generated { + warn!("Generated file changed - clearing handler cache..."); + if let Some(eng) = self.engine.read().await.as_ref() { + if let Err(e) = eng.clear_handler_cache().await { + error!("Failed to clear handler cache: {}", e); + } else { + info!("Handler cache cleared"); + } + } + } else { + warn!("Python handler file changed - clearing cache..."); + if let Some(eng) = self.engine.read().await.as_ref() { + if let Err(e) = eng.clear_handler_cache().await { + error!("Failed to clear handler cache: {}", e); + } else { + info!("Handler cache cleared"); + } + } + } + } else if ext == "rs" { + let path_str = path.to_string_lossy(); + let is_generated = path_str.contains("/generated/") || path_str.contains("\\generated\\"); + + if path_str.ends_with("/lib.rs") || path_str.ends_with("\\lib.rs") { + debug!("Ignoring lib.rs change (touched by build process): {}", path.display()); + continue; + } + if path_str.ends_with("/generated/handlers.rs") || path_str.ends_with("\\generated\\handlers.rs") { + debug!("Ignoring handlers.rs change (touched by build process): {}", path.display()); + continue; + } + + info!("Rust file change detected: {} (generated: {})", path.display(), is_generated); + + if is_generated { + warn!("Generated Rust file changed - recompiling..."); + if let Err(e) = self.reload_rust_handler().await { + error!("Failed to recompile Rust handlers: {}", e); + } else { + info!("Successfully reloaded Rust handlers after generated file change"); + } + } else { + warn!("Rust handler file changed - recompiling..."); + if let Err(e) = self.reload_rust_handler_with_file(Some(path.as_path())).await { + error!("Failed to recompile Rust handlers: {}", e); + warn!("Continuing to watch for changes..."); + } else { + info!("Successfully reloaded Rust handlers after handler file change"); + } + } } } } @@ -378,12 +704,226 @@ impl DevServer { Ok(()) } + async fn reload_rust_handler(&self) -> anyhow::Result<()> { + self.reload_rust_handler_with_file(None).await + } + + async fn reload_rust_handler_with_file(&self, _changed_file: Option<&std::path::Path>) -> anyhow::Result<()> { + { + let rust_compiler = self.rust_compiler.read().await; + if let Some(compiler) = rust_compiler.as_ref() { + info!("Rebuilding Rust handlers as dylib..."); + + let project_root = compiler.project_root().clone(); + let generated_handlers_rs = project_root.join("src").join("generated").join("handlers.rs"); + + if generated_handlers_rs.exists() { + use std::io::Write; + if let Ok(mut file) = fs::OpenOptions::new().append(true).open(&generated_handlers_rs) { + let _ = file.write_all(b"\n"); + drop(file); + info!("Touched generated handlers.rs to force Cargo rebuild: {}", generated_handlers_rs.display()); + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } else { + warn!("Failed to touch generated handlers.rs, but continuing with build"); + } + } else { + warn!("Generated handlers.rs not found at: {}, trying alternative approach", generated_handlers_rs.display()); + let lib_rs = project_root.join("src").join("lib.rs"); + if lib_rs.exists() { + use std::io::Write; + if let Ok(mut file) = fs::OpenOptions::new().append(true).open(&lib_rs) { + let _ = file.write_all(b"\n"); + drop(file); + info!("Touched lib.rs as fallback to force Cargo rebuild"); + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } + } + } + + let build_result = compiler.build_release().await; + build_result?; + + let dylib_path = compiler.get_library_path_for_profile("release")?; + if let Ok(metadata) = dylib_path.metadata() { + if let Ok(modified) = metadata.modified() { + info!("Dylib last modified: {:?}", modified); + } + } + + info!("Rust handlers rebuilt successfully"); + + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } else { + warn!("Rust compiler not initialized, skipping rebuild"); + return Ok(()); + } + } + + { + let engine = self.engine.read().await; + if let Some(eng) = engine.as_ref() { + info!("Clearing Rust handler cache before reload..."); + let handler_count_before = eng.executor().rust_runtime().handler_count().await; + let handlers_before = eng.executor().rust_runtime().list_handlers().await; + info!("Handler count before clearing: {} ({:?})", handler_count_before, handlers_before); + + eng.executor().rust_runtime().clear_handlers().await; + + eng.clear_handler_cache().await?; + + let handler_count_after = eng.executor().rust_runtime().handler_count().await; + let handlers_after = eng.executor().rust_runtime().list_handlers().await; + info!("Handler count after clearing: {} ({:?})", handler_count_after, handlers_after); + if handler_count_after > 0 { + warn!("Some handlers were not cleared! Attempting force clear..."); + eng.executor().rust_runtime().clear_handlers().await; + let final_count = eng.executor().rust_runtime().handler_count().await; + if final_count > 0 { + error!("Handlers still not cleared after force clear! Count: {}", final_count); + } else { + info!("Handlers successfully cleared after force clear"); + } + } + } + } + + { + let mut rust_lib = self.rust_library.lock().await; + if rust_lib.is_some() { + info!("Dropping old Rust dylib..."); + let old_lib = rust_lib.take(); + drop(old_lib); + info!("Old Rust dylib dropped"); + } else { + info!("No old Rust dylib to drop"); + } + } + + info!("Waiting for OS to fully unload old dylib..."); + tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; + info!("Proceeding to load new dylib..."); + + let engine = self.engine.read().await; + if let Some(eng) = engine.as_ref() { + info!("Registering new Rust handlers..."); + + let rust_compiler = self.rust_compiler.read().await; + let (dylib_path, new_dylib_hash) = if let Some(compiler) = rust_compiler.as_ref() { + let dylib_path = compiler.get_library_path_for_profile("release")?; + if !dylib_path.exists() { + return Err(anyhow::anyhow!("Dylib not found at expected path: {}. Build may have failed.", dylib_path.display())); + } + + use sha2::{Sha256, Digest}; + use std::io::Read; + let hash: [u8; 32] = { + let mut file = std::fs::File::open(&dylib_path)?; + let mut hasher = Sha256::new(); + let mut buffer = vec![0u8; 8192]; + loop { + let bytes_read = file.read(&mut buffer)?; + if bytes_read == 0 { + break; + } + hasher.update(&buffer[..bytes_read]); + } + hasher.finalize().into() + }; + + let hash_prefix = u128::from_be_bytes( + hash[..16].try_into().unwrap_or([0; 16]) + ); + info!("New dylib hash after rebuild: 0x{:032x}", hash_prefix); + + let last_hash = { + let last_hash_lock = self.last_loaded_dylib_hash.lock().await; + *last_hash_lock + }; + + if let Some(prev_hash) = last_hash { + if hash == prev_hash { + error!("ERROR: New dylib hash is identical to previously loaded hash!"); + error!("This means the rebuild didn't actually produce new code."); + error!("Hot reload will NOT work - the old code will still be running!"); + return Err(anyhow::anyhow!("Dylib hash unchanged after rebuild - code was not actually rebuilt")); + } else { + let prev_hash_prefix = u128::from_be_bytes( + prev_hash[..16].try_into().unwrap_or([0; 16]) + ); + info!("Dylib hash changed after rebuild: 0x{:032x} -> 0x{:032x} (new code detected)", prev_hash_prefix, hash_prefix); + } + } + + (dylib_path, hash) + } else { + return Err(anyhow::anyhow!("Rust compiler not available")); + }; + + #[cfg(target_os = "macos")] + let dylib_path_to_use = { + use std::time::{SystemTime, UNIX_EPOCH}; + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + + let temp_dir = std::env::temp_dir(); + let temp_dylib_name = format!("librust_example_{}.dylib", timestamp); + let temp_dylib_path = temp_dir.join(&temp_dylib_name); + + info!("Copying dylib to unique temp path to force fresh load: {}", temp_dylib_path.display()); + std::fs::copy(&dylib_path, &temp_dylib_path)?; + info!("Dylib copied successfully"); + + temp_dylib_path + }; + + #[cfg(not(target_os = "macos"))] + let dylib_path_to_use = dylib_path.clone(); + + self.load_and_register_handlers_with_path(&dylib_path_to_use, eng.executor().rust_runtime().clone(), Some(new_dylib_hash)).await?; + + #[cfg(target_os = "macos")] + { + let temp_path = dylib_path_to_use.clone(); + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + if let Err(e) = std::fs::remove_file(&temp_path) { + warn!("Failed to clean up temporary dylib {}: {}", temp_path.display(), e); + } else { + info!("Cleaned up temporary dylib: {}", temp_path.display()); + } + }); + } + + let handler_count = eng.executor().rust_runtime().handler_count().await; + let handlers = eng.executor().rust_runtime().list_handlers().await; + info!("Handler count after registration: {}", handler_count); + info!("Registered handlers: {:?}", handlers); + + if handler_count > 0 { + info!("Handler registration completed - new closures should have been created"); + info!("Note: Function pointer addresses may be the same if macOS reuses memory, but closures should be new"); + } + + if handler_count == 0 { + warn!("No handlers were registered! This indicates a problem."); + } else { + info!("Successfully reloaded {} Rust handler(s)", handler_count); + } + } + + Ok(()) + } + fn run_codegen(&self, schema: &Schema) -> anyhow::Result<()> { let output_dir = self.config.project_root.join("src"); let lang = match self.config.language { EngineLanguage::TypeScript => CodegenLanguage::TypeScript, EngineLanguage::Python => CodegenLanguage::Python, + EngineLanguage::Rust => CodegenLanguage::Rust, }; info!( diff --git a/crates/rohas-dev-server/src/rust_compiler.rs b/crates/rohas-dev-server/src/rust_compiler.rs new file mode 100644 index 0000000..681f549 --- /dev/null +++ b/crates/rohas-dev-server/src/rust_compiler.rs @@ -0,0 +1,390 @@ +use std::path::PathBuf; +use std::process::Command as StdCommand; +use tokio::process::Command; +use std::fs; +use std::io::Read; +use tracing::{error, info, warn}; + +pub struct RustCompiler { + project_root: PathBuf, +} + +impl RustCompiler { + pub fn new(project_root: PathBuf) -> Self { + Self { project_root } + } + + pub fn project_root(&self) -> &PathBuf { + &self.project_root + } + + pub fn is_rust_project(project_root: &PathBuf) -> bool { + project_root.join("Cargo.toml").exists() + } + + pub fn compile(&self) -> anyhow::Result<()> { + info!("Compiling Rust project: {}", self.project_root().display()); + + let output = StdCommand::new("cargo") + .arg("check") + .arg("--message-format=short") + .current_dir(self.project_root()) + .output()?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + error!("Rust compilation failed:\n{}", stderr); + return Err(anyhow::anyhow!("Rust compilation failed")); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + if !stdout.trim().is_empty() { + info!("Compilation output:\n{}", stdout); + } + + info!("Rust project compiled successfully"); + Ok(()) + } + + pub async fn build_release(&self) -> anyhow::Result<()> { + use std::fs; + + info!("Building Rust project in release mode: {}", self.project_root().display()); + + let package_name = self.get_package_name()?; + let project_name = package_name.replace('-', "_"); + + let dylib_path = self.get_library_path_for_profile("release")?; + + let hash_before = if dylib_path.exists() { + let hash = Self::compute_file_hash(&dylib_path)?; + let hash_prefix = u128::from_be_bytes( + hash[..16].try_into().unwrap_or([0; 16]) + ); + info!("Dylib hash before rebuild: 0x{:032x}", hash_prefix); + Some(hash) + } else { + info!("No existing dylib - this will be a fresh build"); + None + }; + + info!("Cleaning release build artifacts for package: {}", package_name); + + if dylib_path.exists() { + if let Err(e) = fs::remove_file(&dylib_path) { + warn!("Failed to delete dylib before clean: {}", e); + } else { + info!("Deleted dylib before clean"); + } + } + + let release_target = self.project_root().join("target").join("release"); + if release_target.exists() { + if let Ok(entries) = fs::read_dir(&release_target.join("deps")) { + let package_pattern = format!("lib{}", project_name); + for entry in entries.flatten() { + let file_name = entry.file_name(); + if let Some(name_str) = file_name.to_str() { + if name_str.starts_with(&package_pattern) { + let _ = fs::remove_file(entry.path()); + } + } + } + } + } + + let target_release = self.project_root().join("target").join("release"); + if target_release.exists() { + info!("Removing entire target/release directory to force full rebuild"); + if let Err(e) = fs::remove_dir_all(&target_release) { + warn!("Failed to remove target/release: {}. Continuing anyway.", e); + } else { + info!("Removed target/release directory"); + } + } + + let clean_output = StdCommand::new("cargo") + .arg("clean") + .arg("--release") + .arg("--package") + .arg(&package_name) + .current_dir(self.project_root()) + .output(); + + match clean_output { + Ok(output) if output.status.success() => { + info!("Cleaned release artifacts for package: {}", project_name); + } + Ok(output) => { + warn!("Package-specific clean failed, trying full clean: {}", + String::from_utf8_lossy(&output.stderr)); + let fallback_clean = StdCommand::new("cargo") + .arg("clean") + .arg("--release") + .current_dir(self.project_root()) + .output(); + if let Ok(fb_output) = fallback_clean { + if fb_output.status.success() { + info!("Cleaned all release artifacts (fallback)"); + } + } + } + Err(e) => { + warn!("Failed to clean release artifacts: {}. Continuing anyway.", e); + } + } + + if dylib_path.exists() { + if let Err(e) = fs::remove_file(&dylib_path) { + warn!("Failed to delete existing dylib before rebuild: {}. Continuing anyway.", e); + } else { + info!("Deleted existing dylib to force rebuild"); + } + } + + for profile in ["debug", "release"] { + let incremental_dir = self.project_root + .join("target") + .join(profile) + .join("incremental"); + if incremental_dir.exists() { + if let Err(e) = fs::remove_dir_all(&incremental_dir) { + warn!("Failed to remove incremental compilation cache for {}: {}. Continuing anyway.", profile, e); + } else { + info!("Removed incremental compilation cache for {}", profile); + } + } + } + + let fingerprint_dir = self.project_root + .join("target") + .join("release") + .join(".fingerprint"); + if fingerprint_dir.exists() { + if let Err(e) = fs::remove_dir_all(&fingerprint_dir) { + warn!("Failed to remove fingerprint directory: {}. Continuing anyway.", e); + } else { + info!("Removed fingerprint directory"); + } + } + + let lib_rs = self.project_root().join("src").join("lib.rs"); + if lib_rs.exists() { + use std::io::Write; + if let Ok(mut file) = fs::OpenOptions::new().append(true).open(&lib_rs) { + let _ = file.write_all(b"\n"); + drop(file); + info!("Touched lib.rs to force Cargo rebuild"); + } + } + + let mut build_cmd = Command::new("cargo"); + build_cmd + .arg("build") + .arg("--release") + .arg("--message-format=short") + .arg("--lib") + .env("CARGO_INCREMENTAL", "0") + .current_dir(self.project_root()); + + let output = build_cmd.output().await?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + error!("Rust release build failed:\n{}", stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + if !stdout.trim().is_empty() { + error!("Build stdout:\n{}", stdout); + } + return Err(anyhow::anyhow!("Rust release build failed")); + } + + if !dylib_path.exists() { + return Err(anyhow::anyhow!("Dylib was not created at expected path: {}", dylib_path.display())); + } + + let dylib_hash = Self::compute_file_hash(&dylib_path)?; + let hash_prefix = u128::from_be_bytes( + dylib_hash[..16].try_into().unwrap_or([0; 16]) + ); + info!("Dylib hash (first 16 bytes of SHA256): 0x{:032x}", hash_prefix); + + if let Some(prev_hash) = hash_before { + if dylib_hash == prev_hash { + warn!("WARNING: Dylib hash did not change after rebuild! This means Cargo did not actually rebuild the code."); + warn!("The dylib binary is identical to before - hot reload will not work!"); + warn!("Attempting to force rebuild by touching generated handlers.rs..."); + + let generated_handlers_rs = self.project_root().join("src").join("generated").join("handlers.rs"); + + if generated_handlers_rs.exists() { + use std::io::Write; + if let Ok(mut file) = fs::OpenOptions::new().append(true).open(&generated_handlers_rs) { + let _ = file.write_all(b"\n"); + drop(file); + info!("Touched generated handlers.rs to force rebuild"); + std::thread::sleep(std::time::Duration::from_millis(100)); + } + } + + let cargo_toml = self.project_root().join("Cargo.toml"); + if cargo_toml.exists() { + use std::io::Write; + if let Ok(mut file) = fs::OpenOptions::new().append(true).open(&cargo_toml) { + let _ = file.write_all(b"\n"); + drop(file); + info!("Touched Cargo.toml to force rebuild"); + } + } + + info!("Rebuilding after touching files..."); + let retry_output = StdCommand::new("cargo") + .arg("build") + .arg("--release") + .arg("--message-format=short") + .arg("--lib") + .env("CARGO_INCREMENTAL", "0") + .current_dir(self.project_root()) + .output()?; + + if !retry_output.status.success() { + let stderr = String::from_utf8_lossy(&retry_output.stderr); + error!("Rust release build failed on retry:\n{}", stderr); + return Err(anyhow::anyhow!("Rust release build failed on retry")); + } + + let retry_hash = Self::compute_file_hash(&dylib_path)?; + if retry_hash == prev_hash { + warn!("Dylib hash still unchanged after touching files - this may indicate:"); + warn!(" 1. The source code hasn't actually changed"); + warn!(" 2. Cargo is using cached artifacts despite cleaning"); + warn!(" 3. The build system is not detecting file changes"); + warn!("Proceeding anyway - hot reload may not work, but the dylib will be reloaded"); + } else { + info!("Dylib hash changed after touching files - rebuild was successful"); + } + } else { + info!("Dylib hash changed - rebuild was successful"); + } + } else { + info!("Dylib built fresh (no previous hash to compare)"); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + let combined_output = format!("{}\n{}", stdout, stderr); + + let was_compiled = combined_output.contains("Compiling") || combined_output.contains("compiling"); + + if was_compiled { + info!("Rust project rebuilt successfully (release mode) - compilation occurred"); + } else if combined_output.contains("Finished") { + warn!("Cargo reported 'Finished' but no compilation occurred - forcing rebuild"); + + let lib_rs = self.project_root().join("src").join("lib.rs"); + if lib_rs.exists() { + use std::fs::OpenOptions; + use std::io::Write; + if let Ok(mut file) = OpenOptions::new().write(true).append(true).open(&lib_rs) { + let _ = file.write_all(b""); + drop(file); + + info!("Forcing rebuild after touching lib.rs..."); + let retry_output = StdCommand::new("cargo") + .arg("build") + .arg("--release") + .arg("--message-format=short") + .arg("--lib") + .current_dir(self.project_root()) + .output()?; + + if !retry_output.status.success() { + let retry_stderr = String::from_utf8_lossy(&retry_output.stderr); + error!("Rust release build failed on retry:\n{}", retry_stderr); + return Err(anyhow::anyhow!("Rust release build failed on retry")); + } + + let retry_stdout = String::from_utf8_lossy(&retry_output.stdout); + if retry_stdout.contains("Compiling") || retry_stdout.contains("compiling") { + info!("Rust project rebuilt successfully after forcing rebuild"); + } else { + warn!("Rebuild completed but still no compilation detected - this may indicate cached build"); + } + } + } + } else { + warn!("Rust project build completed (output unclear)"); + } + Ok(()) + } + + fn get_package_name(&self) -> anyhow::Result { + use std::fs; + + let cargo_toml = self.project_root().join("Cargo.toml"); + let contents = fs::read_to_string(&cargo_toml)?; + + for line in contents.lines() { + let line = line.trim(); + if line.starts_with("name =") { + if let Some(start) = line.find('"') { + if let Some(end) = line.rfind('"') { + if end > start { + return Ok(line[start + 1..end].to_string()); + } + } + } + } + } + + let name = self.project_root + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("rust_example") + .to_string(); + Ok(name) + } + + pub fn get_library_path(&self) -> anyhow::Result { + let profile = if cfg!(debug_assertions) { + "debug" + } else { + "release" + }; + self.get_library_path_for_profile(profile) + } + + pub fn get_library_path_for_profile(&self, profile: &str) -> anyhow::Result { + let target_dir = self.project_root().join("target"); + + let package_name = self.get_package_name()?; + let project_name = package_name.replace('-', "_"); + + #[cfg(target_os = "macos")] + let dylib_name = format!("lib{}.dylib", project_name); + #[cfg(target_os = "linux")] + let dylib_name = format!("lib{}.so", project_name); + #[cfg(target_os = "windows")] + let dylib_name = format!("{}.dll", project_name); + #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] + let dylib_name = format!("lib{}.so", project_name); + + Ok(target_dir.join(profile).join(&dylib_name)) + } + + fn compute_file_hash(path: &PathBuf) -> anyhow::Result<[u8; 32]> { + use sha2::{Sha256, Digest}; + let mut file = fs::File::open(path)?; + let mut hasher = Sha256::new(); + let mut buffer = vec![0u8; 8192]; + loop { + let bytes_read = file.read(&mut buffer)?; + if bytes_read == 0 { + break; + } + hasher.update(&buffer[..bytes_read]); + } + Ok(hasher.finalize().into()) + } +} + diff --git a/crates/rohas-engine/src/api.rs b/crates/rohas-engine/src/api.rs index e2737b7..44051d3 100644 --- a/crates/rohas-engine/src/api.rs +++ b/crates/rohas-engine/src/api.rs @@ -259,6 +259,7 @@ async fn api_handler( let handler_name = match state.config.language { config::Language::TypeScript => api.name.clone(), config::Language::Python => templates::to_snake_case(api.name.clone().as_str()), + config::Language::Rust => templates::to_snake_case(api.name.clone().as_str()), }; let api_path = api.path.clone(); @@ -421,6 +422,7 @@ async fn execute_middlewares( let middleware_handler_name = match state.config.language { config::Language::TypeScript => middleware_name.clone(), config::Language::Python => templates::to_snake_case(middleware_name.as_str()), + config::Language::Rust => templates::to_snake_case(middleware_name.as_str()), }; debug!("Executing middleware: {}", middleware_handler_name); diff --git a/crates/rohas-engine/src/config.rs b/crates/rohas-engine/src/config.rs index 01fa1d8..f0655a9 100644 --- a/crates/rohas-engine/src/config.rs +++ b/crates/rohas-engine/src/config.rs @@ -53,10 +53,11 @@ impl EngineConfig { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Language { TypeScript, Python, + Rust, } impl From for rohas_runtime::Language { @@ -64,6 +65,7 @@ impl From for rohas_runtime::Language { match lang { Language::TypeScript => rohas_runtime::Language::TypeScript, Language::Python => rohas_runtime::Language::Python, + Language::Rust => rohas_runtime::Language::Rust, } } } @@ -264,6 +266,7 @@ impl TomlConfig { let language = match self.project.language.to_lowercase().as_str() { "typescript" | "ts" => Language::TypeScript, "python" | "py" => Language::Python, + "rust" | "rs" => Language::Rust, _ => anyhow::bail!("Unsupported language: {}", self.project.language), }; diff --git a/crates/rohas-engine/src/engine.rs b/crates/rohas-engine/src/engine.rs index 777a2c4..ad2d59a 100644 --- a/crates/rohas-engine/src/engine.rs +++ b/crates/rohas-engine/src/engine.rs @@ -55,7 +55,7 @@ impl Engine { } else { config.project_root.join(&config.telemetry.path) }; - + let telemetry = match config.telemetry.adapter_type { crate::config::TelemetryAdapterType::RocksDB => { Arc::new( @@ -74,7 +74,7 @@ impl Engine { return Err(EngineError::Initialization("TimescaleDB adapter not yet implemented".to_string())); } }; - + let trace_store = Arc::new(crate::telemetry::TraceStore::new(telemetry.clone())); let tracing_log_store = Arc::new(crate::tracing_log::TracingLogStore::new(1000)); // Keep last 1000 logs @@ -145,6 +145,17 @@ impl Engine { return Ok(()); } + // For Rust projects, automatically register handlers + if self.config.language == crate::config::Language::Rust { + let executor_clone = self.executor.clone(); + let project_root = self.config.project_root.clone(); + tokio::spawn(async move { + if let Err(e) = Self::try_auto_register_rust_handlers(&project_root, executor_clone).await { + tracing::warn!("Could not auto-register Rust handlers: {}. Handlers should be registered manually via init_handlers().", e); + } + }); + } + info!("Initializing engine components"); self.event_bus.initialize().await?; @@ -383,6 +394,39 @@ impl Engine { pub fn create_tracing_log_layer(&self) -> crate::tracing_log::TracingLogLayer { crate::tracing_log::TracingLogLayer::new(self.tracing_log_store.clone()) } + + pub fn executor(&self) -> &Arc { + &self.executor + } + + async fn try_auto_register_rust_handlers( + project_root: &PathBuf, + executor: Arc, + ) -> anyhow::Result<()> { + use rohas_runtime::RustRuntime; + use std::sync::Arc; + + let rust_runtime = executor.rust_runtime().clone(); + + let handlers_rs = project_root.join("src").join("generated").join("handlers.rs"); + if !handlers_rs.exists() { + return Ok(()); + } + + let content = std::fs::read_to_string(&handlers_rs)?; + if !content.contains("set_runtime") { + return Ok(()); + } + + tracing::info!("Registering Rust handlers automatically via set_runtime..."); + + tracing::warn!("Automatic Rust handler registration requires calling set_runtime()."); + tracing::warn!("Since the generated project is a separate crate, please call:"); + tracing::warn!(" rust_example::set_runtime(runtime);"); + tracing::warn!("Or: rust_example::init_handlers(runtime).await"); + + Ok(()) + } } #[derive(Debug, Clone, serde::Serialize)] diff --git a/crates/rohas-engine/src/ws.rs b/crates/rohas-engine/src/ws.rs index dbdaac4..b41ff9e 100644 --- a/crates/rohas-engine/src/ws.rs +++ b/crates/rohas-engine/src/ws.rs @@ -27,6 +27,7 @@ async fn execute_websocket_middlewares( let middleware_handler_name = match state.config.language { config::Language::TypeScript => middleware_name.clone(), config::Language::Python => templates::to_snake_case(middleware_name.as_str()), + config::Language::Rust => templates::to_snake_case(middleware_name.as_str()), }; debug!("Executing WebSocket middleware: {}", middleware_handler_name); @@ -132,6 +133,7 @@ pub async fn websocket_handler(socket: WebSocket, state: ApiState, ws_name: Stri let handler_name = match state.config.language { config::Language::TypeScript => handler_name.clone(), config::Language::Python => templates::to_snake_case(handler_name.as_str()), + config::Language::Rust => templates::to_snake_case(handler_name.as_str()), }; let payload = connection.clone(); @@ -217,6 +219,9 @@ pub async fn websocket_handler(socket: WebSocket, state: ApiState, ws_name: Stri config::Language::Python => { templates::to_snake_case(handler_name.as_str()) } + config::Language::Rust => { + templates::to_snake_case(handler_name.as_str()) + } }; let handler_payload = json!({ @@ -379,6 +384,7 @@ pub async fn websocket_handler(socket: WebSocket, state: ApiState, ws_name: Stri let handler_name = match state.config.language { config::Language::TypeScript => handler_name.clone(), config::Language::Python => templates::to_snake_case(handler_name.as_str()), + config::Language::Rust => templates::to_snake_case(handler_name.as_str()), }; let payload = connection.clone(); diff --git a/crates/rohas-parser/src/ast.rs b/crates/rohas-parser/src/ast.rs index 6ead403..cdcc7aa 100644 --- a/crates/rohas-parser/src/ast.rs +++ b/crates/rohas-parser/src/ast.rs @@ -135,6 +135,19 @@ impl FieldType { FieldType::Array(inner) => format!("list[{}]", inner.to_python()), } } + + pub fn to_rust(&self) -> String { + match self { + FieldType::Int => "i64".to_string(), + FieldType::Float => "f64".to_string(), + FieldType::String => "String".to_string(), + FieldType::Boolean => "bool".to_string(), + FieldType::DateTime => "chrono::DateTime".to_string(), + FieldType::Json => "serde_json::Value".to_string(), + FieldType::Custom(name) => name.clone(), + FieldType::Array(inner) => format!("Vec<{}>", inner.to_rust()), + } + } } /// Attribute (e.g., @id, @unique, @default) diff --git a/crates/rohas-runtime/src/executor.rs b/crates/rohas-runtime/src/executor.rs index c5f20b5..1f7cf4f 100644 --- a/crates/rohas-runtime/src/executor.rs +++ b/crates/rohas-runtime/src/executor.rs @@ -2,6 +2,7 @@ use crate::error::{Result, RuntimeError}; use crate::handler::{Handler, HandlerContext, HandlerResult}; use crate::node_runtime::NodeRuntime; use crate::python_runtime::PythonRuntime; +use crate::rust_runtime::RustRuntime; use crate::{Language, RuntimeConfig}; use rohas_codegen::templates; use std::collections::HashMap; @@ -15,6 +16,7 @@ pub struct Executor { handlers: Arc>>>, python_runtime: Arc, node_runtime: Arc, + rust_runtime: Arc, } impl Executor { @@ -27,16 +29,24 @@ impl Executor { node_runtime.set_project_root(config.project_root.clone()); let node_runtime = Arc::new(node_runtime); - info!("Executor initialized with Python and Node.js runtimes"); + let mut rust_runtime = RustRuntime::new().expect("Failed to initialize Rust runtime"); + rust_runtime.set_project_root(config.project_root.clone()); + let rust_runtime = Arc::new(rust_runtime); - Self { - config, + info!("Executor initialized with Python, Node.js, and Rust runtimes"); + + let executor = Self { + config: config.clone(), handlers: Arc::new(RwLock::new(HashMap::new())), python_runtime, node_runtime, - } + rust_runtime: rust_runtime.clone(), + }; + + executor } + pub async fn register_handler(&self, handler: Arc) { let name = handler.name().to_string(); let mut handlers = self.handlers.write().await; @@ -95,6 +105,7 @@ impl Executor { let result = match self.config.language { Language::TypeScript => self.execute_typescript(&handler_path, &context).await, Language::Python => self.execute_python(&handler_path, &context).await, + Language::Rust => self.execute_rust(&handler_path, &context).await, }; let execution_time_ms = start.elapsed().as_millis() as u64; @@ -194,6 +205,17 @@ impl Executor { .await } + async fn execute_rust( + &self, + handler_path: &PathBuf, + context: &HandlerContext, + ) -> Result { + debug!("Executing Rust handler (high-performance): {:?}", handler_path); + self.rust_runtime + .execute_handler(handler_path, context.clone()) + .await + } + pub async fn list_handlers(&self) -> Vec { let handlers = self.handlers.read().await; handlers.keys().cloned().collect() @@ -216,9 +238,22 @@ impl Executor { // @TODO Python runtime doesn't cache modules the same way // Module reloading is handled differently in pyo3 } + Language::Rust => { + self.rust_runtime.clear_handlers().await; + } } Ok(()) } + + /// Get a reference to the Rust runtime for direct handler registration. + /// + /// This allows static handler registration: + /// ```rust + /// executor.rust_runtime().register_handler("my_handler", my_handler_fn).await; + /// ``` + pub fn rust_runtime(&self) -> &Arc { + &self.rust_runtime + } } #[cfg(test)] diff --git a/crates/rohas-runtime/src/lib.rs b/crates/rohas-runtime/src/lib.rs index d3cd8a8..23710bf 100644 --- a/crates/rohas-runtime/src/lib.rs +++ b/crates/rohas-runtime/src/lib.rs @@ -3,10 +3,12 @@ pub mod executor; pub mod handler; pub mod node_runtime; pub mod python_runtime; +pub mod rust_runtime; pub use error::{Result, RuntimeError}; pub use executor::Executor; pub use handler::{Handler, HandlerContext, HandlerResult}; +pub use rust_runtime::RustRuntime; #[derive(Debug, Clone)] pub struct RuntimeConfig { @@ -29,6 +31,7 @@ impl Default for RuntimeConfig { pub enum Language { TypeScript, Python, + Rust, } impl Language { @@ -36,6 +39,7 @@ impl Language { match self { Language::TypeScript => "typescript", Language::Python => "python", + Language::Rust => "rust", } } @@ -43,6 +47,7 @@ impl Language { match self { Language::TypeScript => "ts", Language::Python => "py", + Language::Rust => "rs", } } } diff --git a/crates/rohas-runtime/src/rust_runtime.rs b/crates/rohas-runtime/src/rust_runtime.rs new file mode 100644 index 0000000..970cf0f --- /dev/null +++ b/crates/rohas-runtime/src/rust_runtime.rs @@ -0,0 +1,227 @@ +use crate::error::{Result, RuntimeError}; +use crate::handler::{HandlerContext, HandlerResult}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; +use tokio::sync::RwLock; +use tracing::{debug, info, warn}; + +pub struct RustRuntime { + handlers: Arc>>, + project_root: Arc>>, +} + +type RustHandlerFn = Box< + dyn Fn(HandlerContext) -> std::pin::Pin> + Send>> + Send + Sync, +>; + +impl RustRuntime { + pub fn new() -> Result { + info!("Rust runtime initialized"); + + Ok(Self { + handlers: Arc::new(RwLock::new(HashMap::new())), + project_root: Arc::new(Mutex::new(None)), + }) + } + + pub fn set_project_root(&mut self, root: PathBuf) { + let mut project_root = self.project_root.lock().unwrap(); + *project_root = Some(root); + } + + pub async fn register_handler(&self, name: String, handler: F) + where + F: Fn(HandlerContext) -> Fut + Send + Sync + 'static, + Fut: std::future::Future> + Send + 'static, + { + let handler_fn: RustHandlerFn = Box::new(move |ctx| { + Box::pin(handler(ctx)) + }); + + let mut handlers = self.handlers.write().await; + let was_present = handlers.contains_key(&name); + + let handler_ptr = &handler_fn as *const _ as usize; + if was_present { + if let Some(old_handler) = handlers.get(&name) { + let old_ptr = old_handler as *const _ as usize; + info!("Re-registering Rust handler: {} (old closure ptr: 0x{:x}, new closure ptr: 0x{:x})", name, old_ptr, handler_ptr); + if old_ptr == handler_ptr { + warn!("WARNING: Handler closure pointer is the same! This may indicate the handler wasn't actually replaced."); + } else { + info!("Handler closure pointer changed - old handler should be replaced"); + } + } else { + info!("Re-registering Rust handler: {} (new closure ptr: 0x{:x})", name, handler_ptr); + } + handlers.remove(&name); + } else { + info!("Registering Rust handler: {} (closure ptr: 0x{:x})", name, handler_ptr); + } + + handlers.insert(name.clone(), handler_fn); + } + + pub async fn execute_handler( + &self, + handler_path: &Path, + context: HandlerContext, + ) -> Result { + let start = std::time::Instant::now(); + let handler_name = context.handler_name.clone(); + + debug!("Executing Rust handler: {:?}", handler_path); + + { + let handlers = self.handlers.read().await; + if let Some(handler_fn) = handlers.get(&handler_name) { + let closure_ptr = handler_fn as *const _ as usize; + debug!("Executing registered Rust handler: {} (closure pointer: 0x{:x})", handler_name, closure_ptr); + let result = handler_fn(context).await?; + let execution_time_ms = start.elapsed().as_millis() as u64; + + if let Some(data) = &result.data { + if let Some(data_str) = data.as_str() { + debug!("Handler response snippet: {}...", &data_str[..data_str.len().min(50)]); + } + } + + return Ok(HandlerResult { + execution_time_ms, + ..result + }); + } + } + + self.execute_handler_from_file(handler_path, context, start).await + } + + async fn execute_handler_from_file( + &self, + handler_path: &Path, + context: HandlerContext, + start: std::time::Instant, + ) -> Result { + let execution_time_ms = start.elapsed().as_millis() as u64; + + Err(RuntimeError::HandlerNotFound(format!( + "Rust handler '{}' not found. To register handlers, call the init_handlers function from your generated project.\n\ + Example: rust_example::init_handlers(runtime).await\n\ + Or call: generated::register_all_handlers(runtime).await\n\ + Handler path: {:?}", + context.handler_name, + handler_path + ))) + } + + pub async fn handler_count(&self) -> usize { + let handlers = self.handlers.read().await; + handlers.len() + } + + pub async fn list_handlers(&self) -> Vec { + let handlers = self.handlers.read().await; + handlers.keys().cloned().collect() + } + + pub async fn clear_handlers(&self) { + let mut handlers = self.handlers.write().await; + handlers.clear(); + info!("Cleared all Rust handlers"); + } +} + +impl Default for RustRuntime { + fn default() -> Self { + Self::new().expect("Failed to initialize Rust runtime") + } +} + +/// Helper macro for registering Rust handlers. +/// +/// This macro simplifies handler registration and ensures type safety. +/// +/// # Example +/// ```rust +/// use rohas_runtime::{rust_runtime, HandlerContext, HandlerResult}; +/// +/// async fn my_handler(ctx: HandlerContext) -> Result { +/// // Handler implementation +/// Ok(HandlerResult::success(serde_json::json!({}), 0)) +/// } +/// +/// // Register the handler +/// runtime.register_handler("my_handler".to_string(), my_handler).await; +/// ``` +#[macro_export] +macro_rules! register_rust_handler { + ($runtime:expr, $name:expr, $handler:expr) => { + $runtime.register_handler($name.to_string(), $handler).await + }; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::handler::HandlerContext; + + #[tokio::test] + async fn test_rust_runtime_creation() { + let runtime = RustRuntime::new().unwrap(); + assert_eq!(runtime.handler_count().await, 0); + } + + #[tokio::test] + async fn test_handler_registration() { + let runtime = Arc::new(RustRuntime::new().unwrap()); + + let handler = |ctx: HandlerContext| async move { + Ok(HandlerResult::success( + serde_json::json!({"message": "test"}), + 0, + )) + }; + + runtime + .register_handler("test_handler".to_string(), handler) + .await; + + assert_eq!(runtime.handler_count().await, 1); + + let handlers = runtime.list_handlers().await; + assert_eq!(handlers, vec!["test_handler"]); + } + + #[tokio::test] + async fn test_handler_execution() { + let runtime = Arc::new(RustRuntime::new().unwrap()); + + let handler = |ctx: HandlerContext| async move { + Ok(HandlerResult::success( + serde_json::json!({ + "handler": ctx.handler_name, + "message": "executed" + }), + 0, + )) + }; + + runtime + .register_handler("test_exec".to_string(), handler) + .await; + + let context = HandlerContext::new("test_exec", serde_json::json!({})); + let result = runtime + .execute_handler(Path::new("test.rs"), context) + .await + .unwrap(); + + assert!(result.success); + assert_eq!( + result.data.unwrap()["handler"], + serde_json::json!("test_exec") + ); + } +} + diff --git a/examples/hello-world/pyproject.toml b/examples/hello-world/pyproject.toml index 54f2878..273b3f2 100644 --- a/examples/hello-world/pyproject.toml +++ b/examples/hello-world/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "hello-world" +name = "rohas-app" version = "0.1.0" description = "Rohas event-driven application" requires-python = ">=3.9" diff --git a/examples/hello-world/src/handlers/api/timeline_test_fast.py b/examples/hello-world/src/handlers/api/timeline_test_fast.py index 5d486fd..0cfc6ce 100644 --- a/examples/hello-world/src/handlers/api/timeline_test_fast.py +++ b/examples/hello-world/src/handlers/api/timeline_test_fast.py @@ -5,9 +5,9 @@ async def handle_timeline_test_fast(req: TimelineTestFastRequest, state: State) -> TimelineTestFastResponse: """Fast API handler - completes in ~50ms""" await asyncio.sleep(0.05) # 50ms - + state.logger.info('Fast handler completed') state.trigger_event('FastCompleted', {'duration': 50}) - + return TimelineTestFastResponse(data="Fast operation completed in ~50ms") diff --git a/examples/rust-example/.editorconfig b/examples/rust-example/.editorconfig new file mode 100644 index 0000000..22777ef --- /dev/null +++ b/examples/rust-example/.editorconfig @@ -0,0 +1,24 @@ +# EditorConfig is awesome: https://EditorConfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{ts,tsx,js,jsx,json}] +indent_style = space +indent_size = 2 + +[*.{py}] +indent_style = space +indent_size = 4 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/examples/rust-example/.gitignore b/examples/rust-example/.gitignore new file mode 100644 index 0000000..bdf3be8 --- /dev/null +++ b/examples/rust-example/.gitignore @@ -0,0 +1,52 @@ +# Dependencies +node_modules/ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +env/ +venv/ +ENV/ +.venv/ + +# Build outputs +dist/ +build/ +*.egg-info/ +.tsbuildinfo + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Logs +*.log +logs/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment variables +.env +.env.local +.env.*.local + +# OS +.DS_Store +Thumbs.db + +# Testing +coverage/ +.coverage +.pytest_cache/ +*.cover +.hypothesis/ + +# Rohas compiled output +.rohas/ +src/generated/ diff --git a/examples/rust-example/Cargo.lock b/examples/rust-example/Cargo.lock new file mode 100644 index 0000000..df31524 --- /dev/null +++ b/examples/rust-example/Cargo.lock @@ -0,0 +1,1993 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "calendrical_calculations" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7" +dependencies = [ + "core_maths", + "displaydoc", +] + +[[package]] +name = "cc" +version = "1.2.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "diplomat" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9adb46b05e2f53dcf6a7dfc242e4ce9eb60c369b6b6eb10826a01e93167f59c6" +dependencies = [ + "diplomat_core", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "diplomat-runtime" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0569bd3caaf13829da7ee4e83dbf9197a0e1ecd72772da6d08f0b4c9285c8d29" + +[[package]] +name = "diplomat_core" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51731530ed7f2d4495019abc7df3744f53338e69e2863a6a64ae91821c763df1" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "smallvec", + "strck", + "syn", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "fslock" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + +[[package]] +name = "gzip-header" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2" +dependencies = [ + "crc32fast", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_calendar" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f0e52e009b6b16ba9c0693578796f2dd4aaa59a7f8f920423706714a89ac4e" +dependencies = [ + "calendrical_calculations", + "displaydoc", + "icu_calendar_data", + "icu_locale", + "icu_locale_core", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_calendar_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527f04223b17edfe0bd43baf14a0cb1b017830db65f3950dc00224860a9a446d" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532b11722e350ab6bf916ba6eb0efe3ee54b932666afec989465f9243fe6dd60" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_locale_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f03e2fcaefecdf05619f3d6f91740e79ab969b4dd54f77cbf546b1d0d28e3147" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "serde", + "stable_deref_trait", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "ixdtf" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84de9d95a6d2547d9b77ee3f25fa0ee32e3c3a6484d47a55adebc0439c077992" + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "serde_core", + "writeable", + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d" +dependencies = [ + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6" +dependencies = [ + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "resb" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a067ab3b5ca3b4dc307d0de9cf75f9f5e6ca9717b192b2f28a36c83e5de9e76" +dependencies = [ + "potential_utf", + "serde_core", +] + +[[package]] +name = "rohas-codegen" +version = "0.1.0" +dependencies = [ + "anyhow", + "rohas-parser", + "serde", + "serde_json", + "tera", + "thiserror", + "tracing", +] + +[[package]] +name = "rohas-parser" +version = "0.1.0" +dependencies = [ + "anyhow", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", + "tracing", +] + +[[package]] +name = "rohas-runtime" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "once_cell", + "pyo3", + "rohas-codegen", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "v8", +] + +[[package]] +name = "rust-example" +version = "0.1.0" +dependencies = [ + "chrono", + "rohas-runtime", + "serde", + "serde_json", + "tokio", + "tokio-test", + "tracing", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strck" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42316e70da376f3d113a68d138a60d8a9883c604fe97942721ec2068dab13a9f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "target-lexicon" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" + +[[package]] +name = "temporal_capi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a151e402c2bdb6a3a2a2f3f225eddaead2e7ce7dd5d3fa2090deb11b17aa4ed8" +dependencies = [ + "diplomat", + "diplomat-runtime", + "icu_calendar", + "icu_locale", + "num-traits", + "temporal_rs", + "timezone_provider", + "writeable", + "zoneinfo64", +] + +[[package]] +name = "temporal_rs" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88afde3bd75d2fc68d77a914bece426aa08aa7649ffd0cdd4a11c3d4d33474d1" +dependencies = [ + "core_maths", + "icu_calendar", + "icu_locale", + "ixdtf", + "num-traits", + "timezone_provider", + "tinystr", + "writeable", +] + +[[package]] +name = "tera" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8004bca281f2d32df3bacd59bc67b312cb4c70cea46cbd79dbe8ac5ed206722" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand", + "regex", + "serde", + "serde_json", + "slug", + "unicode-segmentation", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "timezone_provider" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9ba0000e9e73862f3e7ca1ff159e2ddf915c9d8bb11e38a7874760f445d993" +dependencies = [ + "tinystr", + "zerotrie", + "zerovec", + "zoneinfo64", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tracing" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] +name = "v8" +version = "142.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f566072bd76b2631d0dca1d90a766c943863b1fd6b01312281dc919816de976d" +dependencies = [ + "bindgen", + "bitflags", + "fslock", + "gzip-header", + "home", + "miniz_oxide", + "paste", + "temporal_capi", + "which", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "which" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +dependencies = [ + "either", + "home", + "rustix", + "winsafe", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zoneinfo64" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2e5597efbe7c421da8a7fd396b20b571704e787c21a272eecf35dfe9d386f0" +dependencies = [ + "calendrical_calculations", + "icu_locale_core", + "potential_utf", + "resb", + "serde", +] diff --git a/examples/rust-example/Cargo.toml b/examples/rust-example/Cargo.toml new file mode 100644 index 0000000..8da104d --- /dev/null +++ b/examples/rust-example/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "rust-example" +version = "0.1.0" +edition = "2021" + +[workspace] + +[lib] +name = "rust_example" +path = "src/lib.rs" + +[dependencies] +rohas-runtime = { path = "../../crates/rohas-runtime" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tokio = { version = "1.0", features = ["full"] } +chrono = { version = "0.4", features = ["serde"] } +tracing = "0.1" + +[dev-dependencies] +tokio-test = "0.4" diff --git a/examples/rust-example/Makefile b/examples/rust-example/Makefile new file mode 100644 index 0000000..74529aa --- /dev/null +++ b/examples/rust-example/Makefile @@ -0,0 +1,45 @@ +# Makefile for Rohas developers working in examples +# End users: Install rohas CLI and use "rohas dev --workbench" directly + +.PHONY: dev dev-watch codegen check build validate + +# Run development server (for Rohas developers - finds workspace automatically) +# Usage: make dev ARGS="--workbench" +dev: + @./dev.sh $(ARGS) + +# Run development server with workbench +dev-watch: + @./dev.sh --workbench + +# Generate code from schema (for Rohas developers) +codegen: + @SCRIPT_DIR=$$(pwd); \ + WORKSPACE_ROOT=$$SCRIPT_DIR; \ + for i in {1..10}; do \ + if [ -f "$$WORKSPACE_ROOT/Cargo.toml" ] && grep -q "^\[workspace\]" "$$WORKSPACE_ROOT/Cargo.toml" 2>/dev/null && [ -d "$$WORKSPACE_ROOT/crates/rohas-cli" ]; then \ + break; \ + fi; \ + WORKSPACE_ROOT=$$(dirname "$$WORKSPACE_ROOT"); \ + done; \ + cd "$$WORKSPACE_ROOT" && cargo run -p rohas-cli -- codegen --schema "$$SCRIPT_DIR/schema" --output "$$SCRIPT_DIR/src" --lang rust + +# Check Rust code +check: + @CARGO_TARGET_DIR=../../target cargo check + +# Build Rust project +build: + @CARGO_TARGET_DIR=../../target cargo build --release + +# Validate schema (for Rohas developers) +validate: + @SCRIPT_DIR=$$(pwd); \ + WORKSPACE_ROOT=$$SCRIPT_DIR; \ + for i in {1..10}; do \ + if [ -f "$$WORKSPACE_ROOT/Cargo.toml" ] && grep -q "^\[workspace\]" "$$WORKSPACE_ROOT/Cargo.toml" 2>/dev/null && [ -d "$$WORKSPACE_ROOT/crates/rohas-cli" ]; then \ + break; \ + fi; \ + WORKSPACE_ROOT=$$(dirname "$$WORKSPACE_ROOT"); \ + done; \ + cd "$$WORKSPACE_ROOT" && cargo run -p rohas-cli -- validate --schema "$$SCRIPT_DIR/schema" diff --git a/examples/rust-example/README.md b/examples/rust-example/README.md new file mode 100644 index 0000000..e70e2d2 --- /dev/null +++ b/examples/rust-example/README.md @@ -0,0 +1,66 @@ +# rust-example + +Rohas project initialized with rust handlers. + +## Getting Started + +### For End Users + +If you have the `rohas` CLI installed: + +1. Generate code: + ```bash + rohas codegen + ``` + +2. Start development server: + ```bash + rohas dev --workbench + ``` + +3. Validate schema: + ```bash + rohas validate + ``` + +### For Rohas Developers + +If you're developing Rohas itself and working in the examples directory: + +1. Use the development helper script: + ```bash + ./dev.sh --workbench + ``` + + This script automatically finds the workspace root and compiles/runs the CLI. + +2. Or use Make shortcuts: + ```bash + make dev ARGS="--workbench" # Start dev server + make codegen # Generate code + make check # Check Rust code + make validate # Validate schema + ``` + +3. Or from workspace root: + ```bash + cd ../.. + cargo run -p rohas-cli -- dev --workbench --schema examples/rust-example/schema + ``` + +## Project Structure + +- `schema/` - Schema definitions (.ro files) +- `src/handlers/` - Your handler implementations +- `src/generated/` - Auto-generated Rust types (DO NOT EDIT) +- `config/` - Configuration files +- `dev.sh` - Development helper script +- `Makefile` - Development shortcuts + +## Development Workflow + +1. Edit your schema files in `schema/` +2. Run `make codegen` to regenerate types +3. Implement handlers in `src/handlers/` +4. Run `make dev` to start the dev server +5. The server will automatically recompile Rust handlers on file changes diff --git a/examples/rust-example/config/rohas.toml b/examples/rust-example/config/rohas.toml new file mode 100644 index 0000000..455235b --- /dev/null +++ b/examples/rust-example/config/rohas.toml @@ -0,0 +1,33 @@ +[project] +name = "rust-example" +version = "0.1.0" +language = "rust" + +[server] +host = "127.0.0.1" +port = 8000 +enable_cors = true + +[adapter] +type = "memory" +buffer_size = 1000 + +[telemetry] +# Telemetry adapter type: rocksdb (default), prometheus, influxdb, timescaledb +type = "rocksdb" +# Path to telemetry storage (relative to project root or absolute) +path = ".rohas/telemetry" +# Retention period for traces in days (0 = keep forever) +retention_days = 30 +# Maximum number of traces to keep in memory cache +max_cache_size = 1000 +# Enable metrics collection +enable_metrics = true +# Enable logs collection +enable_logs = true +# Enable traces collection +enable_traces = true + +[workbench] +api_key = "2AOMHN7pSNOyS6BeqDer3g==" +allowed_origins = [] diff --git a/examples/rust-example/dev.sh b/examples/rust-example/dev.sh new file mode 100755 index 0000000..c04f5cd --- /dev/null +++ b/examples/rust-example/dev.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# Development helper script for Rohas developers +# For end users: install rohas CLI and run "rohas dev --workbench" directly + +set -e + +# Find the workspace root (look for Cargo.toml with [workspace]) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKSPACE_ROOT="$SCRIPT_DIR" + +# Look for workspace root (go up to 10 levels to handle nested examples) +for i in {1..10}; do + if [ -f "$WORKSPACE_ROOT/Cargo.toml" ]; then + # Check if it's a workspace (has [workspace] and contains crates/rohas-cli) + if grep -q "^\[workspace\]" "$WORKSPACE_ROOT/Cargo.toml" 2>/dev/null && \ + [ -d "$WORKSPACE_ROOT/crates/rohas-cli" ]; then + break + fi + fi + WORKSPACE_ROOT="$(dirname "$WORKSPACE_ROOT")" + # Stop if we've reached the filesystem root + if [ "$WORKSPACE_ROOT" = "/" ] || [ "$WORKSPACE_ROOT" = "$SCRIPT_DIR" ]; then + break + fi +done + +if [ -f "$WORKSPACE_ROOT/Cargo.toml" ] && grep -q "^\[workspace\]" "$WORKSPACE_ROOT/Cargo.toml" 2>/dev/null && \ + [ -d "$WORKSPACE_ROOT/crates/rohas-cli" ]; then + cd "$WORKSPACE_ROOT" + REL_SCHEMA_PATH=$(python3 -c "import os; print(os.path.relpath('$SCRIPT_DIR/schema', '$WORKSPACE_ROOT'))" 2>/dev/null || \ + perl -MFile::Spec -e "print File::Spec->abs2rel('$SCRIPT_DIR/schema', '$WORKSPACE_ROOT')" 2>/dev/null || \ + echo "schema") + # Check if --schema argument is already provided + HAS_SCHEMA_ARG=false + for arg in "$@"; do + if [[ "$arg" == "--schema" ]] || [[ "$arg" == "-s" ]]; then + HAS_SCHEMA_ARG=true + break + fi + done + # If no schema arg provided, add it + if [ "$HAS_SCHEMA_ARG" = false ]; then + exec cargo run -p rohas-cli -- dev --schema "$REL_SCHEMA_PATH" "$@" + else + exec cargo run -p rohas-cli -- dev "$@" + fi +else + # Not in workspace - try installed binary or show helpful error + if command -v rohas >/dev/null 2>&1; then + cd "$SCRIPT_DIR" + exec rohas dev "$@" + else + echo "Error: Could not find Rohas workspace root and rohas CLI is not installed" + echo "" + echo "For Rohas developers: Run this script from within the rohas workspace" + echo "For end users: Install rohas CLI first:" + echo " cargo install --path /crates/rohas-cli" + echo " Then run: rohas dev --workbench" + exit 1 + fi +fi diff --git a/examples/rust-example/schema/api/user_api.ro b/examples/rust-example/schema/api/user_api.ro new file mode 100644 index 0000000..6f5897f --- /dev/null +++ b/examples/rust-example/schema/api/user_api.ro @@ -0,0 +1,19 @@ +input CreateUserInput { + name: String + email: String +} + +api CreateUser { + method: POST + path: "/users" + body: CreateUserInput + response: User + triggers: [UserCreated] +} + + +api HelloWorld { + method: GET + path: "/hello-world" + response: String +} diff --git a/examples/rust-example/schema/events/user_events.ro b/examples/rust-example/schema/events/user_events.ro new file mode 100644 index 0000000..4fd7728 --- /dev/null +++ b/examples/rust-example/schema/events/user_events.ro @@ -0,0 +1,4 @@ +event UserCreated { + payload: User + handler: [send_welcome_email] +} diff --git a/examples/rust-example/schema/models/user.ro b/examples/rust-example/schema/models/user.ro new file mode 100644 index 0000000..7325a83 --- /dev/null +++ b/examples/rust-example/schema/models/user.ro @@ -0,0 +1,6 @@ +model User { + id Int @id @auto + name String + email String @unique + createdAt DateTime @default(now) +} diff --git a/examples/rust-example/src/handlers/api/create_user.rs b/examples/rust-example/src/handlers/api/create_user.rs new file mode 100644 index 0000000..1e530c7 --- /dev/null +++ b/examples/rust-example/src/handlers/api/create_user.rs @@ -0,0 +1,21 @@ +use crate::generated::api::create_user::{ CreateUserRequest, CreateUserResponse }; +use crate::generated::state::State; +use rohas_runtime::Result; + +/// High-performance Rust handler for CreateUser API. +pub async fn handle_create_user( + req: CreateUserRequest, + state: &mut State, +) -> Result { + state.logger().info("CreateUser handler called"); + state.logger().info(&format!("Request: {:?}", req)); + state.logger().info(&format!("State: {:?}", state)); + + // Create a new user from the request + Ok(CreateUserResponse { + id: 1, // TODO: Generate proper ID + name: req.name.clone(), + email: req.email.clone(), + createdAt: chrono::Utc::now(), + }) +} diff --git a/examples/rust-example/src/handlers/api/hello_world.rs b/examples/rust-example/src/handlers/api/hello_world.rs new file mode 100644 index 0000000..cdc1f99 --- /dev/null +++ b/examples/rust-example/src/handlers/api/hello_world.rs @@ -0,0 +1,13 @@ +use crate::generated::api::hello_world::{ HelloWorldRequest, HelloWorldResponse }; +use crate::generated::state::State; +use rohas_runtime::{HandlerContext, HandlerResult, Result}; + +/// High-performance Rust handler for HelloWorld API. +pub async fn handle_hello_world( + req: HelloWorldRequest, + state: &mut State, +) -> Result { + state.logger().info("Hello Worlds"); + Ok("hello qqwqwqwsadsd".to_string()) +} + diff --git a/examples/rust-example/src/handlers/api/mod.rs b/examples/rust-example/src/handlers/api/mod.rs new file mode 100644 index 0000000..3282ecc --- /dev/null +++ b/examples/rust-example/src/handlers/api/mod.rs @@ -0,0 +1,4 @@ +// API handler modules + +pub mod create_user; +pub mod hello_world; diff --git a/examples/rust-example/src/handlers/events/mod.rs b/examples/rust-example/src/handlers/events/mod.rs new file mode 100644 index 0000000..4f5204e --- /dev/null +++ b/examples/rust-example/src/handlers/events/mod.rs @@ -0,0 +1,3 @@ +// Event handler modules + +pub mod send_welcome_email; diff --git a/examples/rust-example/src/handlers/events/send_welcome_email.rs b/examples/rust-example/src/handlers/events/send_welcome_email.rs new file mode 100644 index 0000000..1af9c0b --- /dev/null +++ b/examples/rust-example/src/handlers/events/send_welcome_email.rs @@ -0,0 +1,11 @@ +use crate::generated::events::user_created::UserCreated; +use rohas_runtime::{HandlerContext, HandlerResult, Result}; + +/// High-performance Rust event handler. +pub async fn send_welcome_email( + event: UserCreated, +) -> Result { + // TODO: Implement event handler + tracing::info!("Handling event: {:?}", event); + Ok(HandlerResult::success(serde_json::json!({}), 0)) +} diff --git a/examples/rust-example/src/handlers/mod.rs b/examples/rust-example/src/handlers/mod.rs new file mode 100644 index 0000000..a06481c --- /dev/null +++ b/examples/rust-example/src/handlers/mod.rs @@ -0,0 +1,4 @@ +// Handler module declarations + +pub mod api; +pub mod events; diff --git a/examples/rust-example/src/lib.rs b/examples/rust-example/src/lib.rs new file mode 100644 index 0000000..6b31576 --- /dev/null +++ b/examples/rust-example/src/lib.rs @@ -0,0 +1,51 @@ +// Main library entry point for Rohas Rust application +// This file sets up the module structure + +#[path = "generated/lib.rs"] +pub mod generated; + +// Re-export generated types for convenience +pub use generated::*; + +pub mod handlers; + +/// Initialize and register all handlers with the Rust runtime. +/// This function should be called during engine startup. +/// It will automatically register all handlers using the global registry. +pub async fn init_handlers(runtime: std::sync::Arc) -> rohas_runtime::Result<()> { + generated::register_all_handlers(runtime).await +} + +/// C-compatible FFI function for automatic handler registration. +/// This is called automatically by the engine. +/// Returns 0 on success, non-zero on error. +#[no_mangle] +pub extern "C" fn rohas_set_runtime(runtime_ptr: *mut std::ffi::c_void) -> i32 { + use std::sync::Arc; + + if runtime_ptr.is_null() { + return 1; // Error: null pointer + } + + // Safety: The engine passes a valid Arc pointer that was created with Arc::into_raw. + // We reconstruct the Arc temporarily to clone it, then forget it so the engine retains ownership. + unsafe { + // Convert the raw pointer back to Arc + // The engine created this with Arc::into_raw, so we reconstruct it temporarily + let runtime: Arc = Arc::from_raw(runtime_ptr as *const rohas_runtime::RustRuntime); + + // Clone the Arc - this increments the reference count + let runtime_clone = runtime.clone(); + + // Forget the reconstructed Arc - we don't want to drop it here since the engine still owns it + // The engine will manage the original Arc's lifetime + std::mem::forget(runtime); + + // Call the generated set_runtime function which will register all handlers + // This will store the cloned Arc in a OnceLock and register handlers synchronously + // Note: If registration fails, set_runtime will panic (via .expect()) + generated::set_runtime(runtime_clone); + + 0 // Success + } +} From 912cd0e4003c17c37ddaa13d7aac3281cf9fb7e2 Mon Sep 17 00:00:00 2001 From: sophatvathana Date: Fri, 5 Dec 2025 10:00:20 +0700 Subject: [PATCH 2/3] docs(executor): enhance example for handler registration in rust_runtime documentation --- crates/rohas-runtime/src/executor.rs | 13 ++++++++++++- crates/rohas-runtime/src/rust_runtime.rs | 9 +++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/rohas-runtime/src/executor.rs b/crates/rohas-runtime/src/executor.rs index 1f7cf4f..72bac40 100644 --- a/crates/rohas-runtime/src/executor.rs +++ b/crates/rohas-runtime/src/executor.rs @@ -249,7 +249,18 @@ impl Executor { /// /// This allows static handler registration: /// ```rust - /// executor.rust_runtime().register_handler("my_handler", my_handler_fn).await; + /// use rohas_runtime::{Executor, RuntimeConfig, HandlerContext, HandlerResult}; + /// + /// # async fn example() -> Result<(), Box> { + /// let executor = Executor::new(RuntimeConfig::default()); + /// + /// async fn my_handler_fn(ctx: HandlerContext) -> rohas_runtime::error::Result { + /// Ok(HandlerResult::success(serde_json::json!({}), 0)) + /// } + /// + /// executor.rust_runtime().register_handler("my_handler".to_string(), my_handler_fn).await; + /// # Ok(()) + /// # } /// ``` pub fn rust_runtime(&self) -> &Arc { &self.rust_runtime diff --git a/crates/rohas-runtime/src/rust_runtime.rs b/crates/rohas-runtime/src/rust_runtime.rs index 970cf0f..3a380a2 100644 --- a/crates/rohas-runtime/src/rust_runtime.rs +++ b/crates/rohas-runtime/src/rust_runtime.rs @@ -144,15 +144,20 @@ impl Default for RustRuntime { /// /// # Example /// ```rust -/// use rohas_runtime::{rust_runtime, HandlerContext, HandlerResult}; +/// use rohas_runtime::{RustRuntime, HandlerContext, HandlerResult}; /// -/// async fn my_handler(ctx: HandlerContext) -> Result { +/// # async fn example() -> Result<(), Box> { +/// let runtime = RustRuntime::new()?; +/// +/// async fn my_handler(ctx: HandlerContext) -> rohas_runtime::error::Result { /// // Handler implementation /// Ok(HandlerResult::success(serde_json::json!({}), 0)) /// } /// /// // Register the handler /// runtime.register_handler("my_handler".to_string(), my_handler).await; +/// # Ok(()) +/// # } /// ``` #[macro_export] macro_rules! register_rust_handler { From a001ccbee2e1736b7c6f87250c8d9b2b546d3bc8 Mon Sep 17 00:00:00 2001 From: sophatvathana Date: Fri, 5 Dec 2025 10:01:48 +0700 Subject: [PATCH 3/3] chore(.gitignore): add MacOS specific files to ignore list --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index da2259d..2e744df 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ target # Contains mutation testing data **/mutants.out*/ +# MacOS specific files +.DS_Store + # RustRover # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore