From 1176d3f9c7eebbd7abcda4ecf07d70a4c1eb9051 Mon Sep 17 00:00:00 2001 From: sophatvathana Date: Sat, 6 Dec 2025 18:34:27 +0700 Subject: [PATCH] fix(rust): enhance Rust code generation with improved error handling, dynamic directory creation, and support for WebSocket handlers and middlewares --- crates/rohas-codegen/src/config.rs | 136 ++++- crates/rohas-codegen/src/generator.rs | 65 ++- crates/rohas-codegen/src/rust.rs | 473 +++++++++++++++++- crates/rohas-dev-server/src/rust_compiler.rs | 116 ++++- examples/rust-example/Cargo.lock | 6 + examples/rust-example/Cargo.toml | 2 +- examples/rust-example/schema/api/user_api.ro | 16 + .../src/handlers/middlewares/auth.rs | 13 + .../src/handlers/middlewares/rate_limit.rs | 13 + .../handlers/middlewares/request_logging.rs | 13 + examples/rust-example/src/handlers/mod.rs | 1 + .../src/handlers/websockets/mod.rs | 5 + .../handlers/websockets/on_connect_handler.rs | 13 + .../websockets/on_disconnect_handler.rs | 12 + .../handlers/websockets/on_message_handler.rs | 14 + examples/rust-example/src/lib.rs | 14 +- examples/rust-example/src/middlewares/auth.rs | 13 + examples/rust-example/src/middlewares/mod.rs | 5 + .../src/middlewares/rate_limit.rs | 13 + .../src/middlewares/request_logging.rs | 13 + 20 files changed, 906 insertions(+), 50 deletions(-) create mode 100644 examples/rust-example/src/handlers/middlewares/auth.rs create mode 100644 examples/rust-example/src/handlers/middlewares/rate_limit.rs create mode 100644 examples/rust-example/src/handlers/middlewares/request_logging.rs create mode 100644 examples/rust-example/src/handlers/websockets/mod.rs create mode 100644 examples/rust-example/src/handlers/websockets/on_connect_handler.rs create mode 100644 examples/rust-example/src/handlers/websockets/on_disconnect_handler.rs create mode 100644 examples/rust-example/src/handlers/websockets/on_message_handler.rs create mode 100644 examples/rust-example/src/middlewares/auth.rs create mode 100644 examples/rust-example/src/middlewares/mod.rs create mode 100644 examples/rust-example/src/middlewares/rate_limit.rs create mode 100644 examples/rust-example/src/middlewares/request_logging.rs diff --git a/crates/rohas-codegen/src/config.rs b/crates/rohas-codegen/src/config.rs index 8e7395a..bb30853 100644 --- a/crates/rohas-codegen/src/config.rs +++ b/crates/rohas-codegen/src/config.rs @@ -4,7 +4,7 @@ use std::fs; use std::path::{Path, PathBuf}; pub fn generate_package_json(_schema: &Schema, output_dir: &Path) -> Result<()> { - let project_root = get_project_root(output_dir); + let project_root = get_project_root(output_dir)?; let project_name = extract_project_name(&project_root); let content = format!( @@ -45,7 +45,7 @@ pub fn generate_package_json(_schema: &Schema, output_dir: &Path) -> Result<()> } pub fn generate_tsconfig_json(_schema: &Schema, output_dir: &Path) -> Result<()> { - let project_root = get_project_root(output_dir); + let project_root = get_project_root(output_dir)?; let content = r#"{ "compilerOptions": { "target": "ES2022", @@ -88,7 +88,7 @@ pub fn generate_tsconfig_json(_schema: &Schema, output_dir: &Path) -> Result<()> } pub fn generate_requirements_txt(_schema: &Schema, output_dir: &Path) -> Result<()> { - let project_root = get_project_root(output_dir); + let project_root = get_project_root(output_dir)?; let content = r#"# Python dependencies for Rohas project # Add your project-specific dependencies here @@ -102,7 +102,7 @@ typing-extensions>=4.0.0 } pub fn generate_pyproject_toml(_schema: &Schema, output_dir: &Path) -> Result<()> { - let project_root = get_project_root(output_dir); + let project_root = get_project_root(output_dir)?; let project_name = extract_project_name(&project_root); let content = format!( @@ -146,7 +146,7 @@ target-version = "py39" } pub fn generate_cargo_toml(_schema: &Schema, output_dir: &Path) -> Result<()> { - let project_root = get_project_root(output_dir); + let project_root = get_project_root(output_dir)?; let project_name = extract_project_name(&project_root); let lib_name = project_name.replace('-', "_"); @@ -183,7 +183,28 @@ tokio-test = "0.4" } pub fn generate_gitignore(_schema: &Schema, output_dir: &Path) -> Result<()> { - let project_root = get_project_root(output_dir); + let project_root = get_project_root(output_dir) + .map_err(|e| crate::error::CodegenError::GenerationFailed(format!( + "Failed to get project root from output_dir {}: {}", + output_dir.display(), + e + )))?; + + let gitignore_path = project_root.join(".gitignore"); + + if let Some(parent) = gitignore_path.parent() { + fs::create_dir_all(parent).map_err(|e| { + crate::error::CodegenError::Io(std::io::Error::new( + e.kind(), + format!( + "Failed to create parent directory {} for .gitignore: {}", + parent.display(), + e + ) + )) + })?; + } + let content = r#"# Dependencies node_modules/ __pycache__/ @@ -238,12 +259,22 @@ coverage/ src/generated/ "#; - fs::write(project_root.join(".gitignore"), content)?; + fs::write(&gitignore_path, content) + .map_err(|e| crate::error::CodegenError::Io(std::io::Error::new( + e.kind(), + format!("Failed to write .gitignore to {}: {}", gitignore_path.display(), e) + )))?; Ok(()) } pub fn generate_editorconfig(_schema: &Schema, output_dir: &Path) -> Result<()> { - let project_root = get_project_root(output_dir); + let project_root = get_project_root(output_dir)?; + let editorconfig_path = project_root.join(".editorconfig"); + + if let Some(parent) = editorconfig_path.parent() { + fs::create_dir_all(parent)?; + } + let content = r#"# EditorConfig is awesome: https://EditorConfig.org root = true @@ -270,12 +301,21 @@ indent_size = 2 trim_trailing_whitespace = false "#; - fs::write(project_root.join(".editorconfig"), content)?; + fs::write(&editorconfig_path, content) + .map_err(|e| crate::error::CodegenError::Io(std::io::Error::new( + e.kind(), + format!("Failed to write .editorconfig to {}: {}", editorconfig_path.display(), e) + )))?; Ok(()) } pub fn generate_readme(schema: &Schema, output_dir: &Path) -> Result<()> { - let project_root = get_project_root(output_dir); + let project_root = get_project_root(output_dir) + .map_err(|e| crate::error::CodegenError::GenerationFailed(format!( + "Failed to get project root from output_dir {} in generate_readme: {}", + output_dir.display(), + e + )))?; let project_name = extract_project_name(&project_root); let has_apis = !schema.apis.is_empty(); let has_events = !schema.events.is_empty(); @@ -400,22 +440,31 @@ MIT ); let readme_path = project_root.join("README.md"); + + if let Some(parent) = readme_path.parent() { + fs::create_dir_all(parent)?; + } + if !readme_path.exists() { - fs::write(readme_path, content)?; + fs::write(&readme_path, content) + .map_err(|e| crate::error::CodegenError::Io(std::io::Error::new( + e.kind(), + format!("Failed to write README.md to {}: {}", readme_path.display(), e) + )))?; } Ok(()) } pub fn generate_nvmrc(_schema: &Schema, output_dir: &Path) -> Result<()> { - let project_root = get_project_root(output_dir); + let project_root = get_project_root(output_dir)?; let content = "18.0.0\n"; fs::write(project_root.join(".nvmrc"), content)?; Ok(()) } pub fn generate_prettierrc(_schema: &Schema, output_dir: &Path) -> Result<()> { - let project_root = get_project_root(output_dir); + let project_root = get_project_root(output_dir)?; let content = r#"{ "semi": true, "trailingComma": "es5", @@ -432,7 +481,7 @@ pub fn generate_prettierrc(_schema: &Schema, output_dir: &Path) -> Result<()> { } pub fn generate_prettierignore(_schema: &Schema, output_dir: &Path) -> Result<()> { - let project_root = get_project_root(output_dir); + let project_root = get_project_root(output_dir)?; let content = r#"node_modules/ dist/ build/ @@ -447,7 +496,7 @@ src/generated/ } pub fn generate_rspack_config(_schema: &Schema, output_dir: &Path) -> Result<()> { - let project_root = get_project_root(output_dir); + let project_root = get_project_root(output_dir)?; let content = r#"const path = require('path'); const fs = require('fs'); @@ -548,12 +597,63 @@ module.exports = { Ok(()) } -fn get_project_root(output_dir: &Path) -> PathBuf { - if output_dir.file_name().and_then(|s| s.to_str()) == Some("src") { - output_dir.parent().unwrap_or(output_dir).to_path_buf() +fn get_project_root(output_dir: &Path) -> Result { + let project_root = if output_dir.file_name().and_then(|s| s.to_str()) == Some("src") { + match output_dir.parent() { + Some(parent) => { + let parent_path = parent.to_path_buf(); + if parent_path.as_os_str().is_empty() || parent_path == Path::new("/") { + output_dir.to_path_buf() + } else { + parent_path + } + } + None => { + return Err(crate::error::CodegenError::GenerationFailed(format!( + "Cannot determine project root from output_dir: {}", + output_dir.display() + ))); + } + } } else { output_dir.to_path_buf() + }; + match fs::metadata(&project_root) { + Ok(metadata) => { + if !metadata.is_dir() { + return Err(crate::error::CodegenError::GenerationFailed(format!( + "Project root path exists but is not a directory: {}", + project_root.display() + ))); + } + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + fs::create_dir_all(&project_root).map_err(|e| { + crate::error::CodegenError::Io(std::io::Error::new( + e.kind(), + format!( + "Failed to create project root directory {} (from output_dir {}): {}", + project_root.display(), + output_dir.display(), + e + ) + )) + })?; + } + Err(e) => { + return Err(crate::error::CodegenError::Io(std::io::Error::new( + e.kind(), + format!( + "Failed to check project root directory {} (from output_dir {}): {}", + project_root.display(), + output_dir.display(), + e + ) + ))); + } } + + Ok(project_root) } fn extract_project_name(project_root: &Path) -> String { diff --git a/crates/rohas-codegen/src/generator.rs b/crates/rohas-codegen/src/generator.rs index 98f6d05..677fc5d 100644 --- a/crates/rohas-codegen/src/generator.rs +++ b/crates/rohas-codegen/src/generator.rs @@ -21,6 +21,11 @@ impl Generator { output_dir.display() ); + if let Some(parent) = output_dir.parent() { + fs::create_dir_all(parent)?; + } + fs::create_dir_all(output_dir)?; + self.create_directory_structure(output_dir)?; self.generate_common_configs(schema, output_dir)?; @@ -64,10 +69,43 @@ impl Generator { } fn generate_common_configs(&self, schema: &Schema, output_dir: &Path) -> Result<()> { + use tracing::error; info!("Generating common configuration files"); - config::generate_gitignore(schema, output_dir)?; - config::generate_editorconfig(schema, output_dir)?; - config::generate_readme(schema, output_dir)?; + info!("Output directory: {}", output_dir.display()); + + info!("Generating .gitignore..."); + config::generate_gitignore(schema, output_dir) + .map_err(|e| { + error!("Failed to generate .gitignore: {}", e); + crate::error::CodegenError::GenerationFailed(format!( + "Failed to generate .gitignore: {}", + e + )) + })?; + info!("Generated .gitignore successfully"); + + info!("Generating .editorconfig..."); + config::generate_editorconfig(schema, output_dir) + .map_err(|e| { + error!("Failed to generate .editorconfig: {}", e); + crate::error::CodegenError::GenerationFailed(format!( + "Failed to generate .editorconfig: {}", + e + )) + })?; + info!("Generated .editorconfig successfully"); + + info!("Generating README.md..."); + config::generate_readme(schema, output_dir) + .map_err(|e| { + error!("Failed to generate README.md: {}", e); + crate::error::CodegenError::GenerationFailed(format!( + "Failed to generate README.md: {}", + e + )) + })?; + info!("Generated README.md successfully"); + Ok(()) } @@ -112,14 +150,33 @@ impl Generator { } fn generate_rust(&self, schema: &Schema, output_dir: &Path) -> Result<()> { + use tracing::error; + + info!("Generating Rust code..."); + info!("Generating state..."); rust::generate_state(output_dir)?; + info!("Generating models..."); rust::generate_models(schema, output_dir)?; + info!("Generating DTOs..."); rust::generate_dtos(schema, output_dir)?; + info!("Generating APIs..."); rust::generate_apis(schema, output_dir)?; + info!("Generating events..."); rust::generate_events(schema, output_dir)?; + info!("Generating crons..."); rust::generate_crons(schema, output_dir)?; - rust::generate_websockets(schema, output_dir)?; + info!("Generating websockets..."); + rust::generate_websockets(schema, output_dir) + .map_err(|e| { + error!("Failed to generate websockets: {}", e); + crate::error::CodegenError::GenerationFailed(format!( + "Failed to generate websockets: {}", + e + )) + })?; + info!("Generating middlewares..."); rust::generate_middlewares(schema, output_dir)?; + info!("Generating lib.rs..."); rust::generate_lib_rs(schema, output_dir)?; info!("Generating Rust configuration files"); diff --git a/crates/rohas-codegen/src/rust.rs b/crates/rohas-codegen/src/rust.rs index b276c83..33970cc 100644 --- a/crates/rohas-codegen/src/rust.rs +++ b/crates/rohas-codegen/src/rust.rs @@ -4,6 +4,22 @@ use rohas_parser::{Api, Event, FieldType, Model, Schema, WebSocket}; use std::fs; use std::path::Path; +/// Rust reserved keywords that need to be escaped with r# +const RUST_RESERVED_KEYWORDS: &[&str] = &[ + "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", + "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", + "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait", "true", + "type", "unsafe", "use", "where", "while", +]; + +fn escape_rust_keyword(name: &str) -> String { + if RUST_RESERVED_KEYWORDS.contains(&name) { + format!("r#{}", name) + } else { + name.to_string() + } +} + pub fn generate_models(schema: &Schema, output_dir: &Path) -> Result<()> { let models_dir = output_dir.join("generated/models"); @@ -41,7 +57,13 @@ fn generate_model_content(model: &Model) -> String { rust_type }; - let field_name = &field.name; + let field_name = escape_rust_keyword(&field.name); + let serde_attr = if RUST_RESERVED_KEYWORDS.contains(&field.name.as_str()) { + format!(" #[serde(rename = \"{}\")]\n", field.name) + } else { + String::new() + }; + content.push_str(&serde_attr); content.push_str(&format!(" pub {}: {},\n", field_name, type_hint)); } @@ -119,9 +141,9 @@ fn generate_api_content(api: &Api) -> String { 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)); + content.push_str(&format!("use crate::generated::dto::{}::{};\n", body_type_snake, body_type)); } else { - content.push_str(&format!("use super::super::models::{}::{};\n", body_type_snake, body_type)); + content.push_str(&format!("use crate::generated::models::{}::{};\n", body_type_snake, body_type)); } } @@ -129,7 +151,7 @@ fn generate_api_content(api: &Api) -> String { 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(&format!("use crate::generated::models::{}::{};\n", response_type_snake, api.response)); } content.push_str("\n"); @@ -333,11 +355,23 @@ fn generate_cron_handler_stub(cron: &rohas_parser::Cron) -> String { pub fn generate_websockets(schema: &Schema, output_dir: &Path) -> Result<()> { let ws_dir = output_dir.join("generated/websockets"); + + fs::create_dir_all(&ws_dir)?; for ws in &schema.websockets { - let content = generate_websocket_content(ws); + let content = generate_websocket_content(ws, schema); let file_name = format!("{}.rs", templates::to_snake_case(&ws.name)); - fs::write(ws_dir.join(file_name), content)?; + let file_path = ws_dir.join(&file_name); + fs::write(&file_path, content).map_err(|e| { + crate::error::CodegenError::Io(std::io::Error::new( + e.kind(), + format!( + "Failed to write websocket file {}: {}", + file_path.display(), + e + ) + )) + })?; } let mut mod_content = String::new(); @@ -354,13 +388,24 @@ pub fn generate_websockets(schema: &Schema, output_dir: &Path) -> Result<()> { fs::write(ws_dir.join("mod.rs"), mod_content)?; let handlers_dir = output_dir.join("handlers/websockets"); + fs::create_dir_all(&handlers_dir)?; + 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)?; + fs::write(&handler_path, content).map_err(|e| { + crate::error::CodegenError::Io(std::io::Error::new( + e.kind(), + format!( + "Failed to write websocket handler {}: {}", + handler_path.display(), + e + ) + )) + })?; } } for handler in &ws.on_message { @@ -368,7 +413,16 @@ pub fn generate_websockets(schema: &Schema, output_dir: &Path) -> Result<()> { 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)?; + fs::write(&handler_path, content).map_err(|e| { + crate::error::CodegenError::Io(std::io::Error::new( + e.kind(), + format!( + "Failed to write websocket handler {}: {}", + handler_path.display(), + e + ) + )) + })?; } } for handler in &ws.on_disconnect { @@ -376,7 +430,16 @@ pub fn generate_websockets(schema: &Schema, output_dir: &Path) -> Result<()> { 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)?; + fs::write(&handler_path, content).map_err(|e| { + crate::error::CodegenError::Io(std::io::Error::new( + e.kind(), + format!( + "Failed to write websocket handler {}: {}", + handler_path.display(), + e + ) + )) + })?; } } } @@ -384,18 +447,49 @@ pub fn generate_websockets(schema: &Schema, output_dir: &Path) -> Result<()> { Ok(()) } -fn generate_websocket_content(ws: &WebSocket) -> String { +fn generate_websocket_content(ws: &WebSocket, schema: &Schema) -> String { let mut content = String::new(); - content.push_str("use serde::{Deserialize, Serialize};\n\n"); + content.push_str("use serde::{Deserialize, Serialize};\n"); + if ws.message.is_some() { + content.push_str("use chrono::{DateTime, Utc};\n"); + } + content.push_str("\n"); if let Some(message_type) = &ws.message { - let rust_type = FieldType::from_str(message_type).to_rust(); + let message_field_type = FieldType::from_str(message_type); + let is_custom_type = matches!(message_field_type, FieldType::Custom(_)); + + let rust_type = message_field_type.to_rust(); + + if is_custom_type { + let message_type_snake = templates::to_snake_case(message_type); + // Check if it's an input/DTO type + let is_input = schema.inputs.iter().any(|input| input.name == *message_type) + || message_type.ends_with("Input"); + + if is_input { + content.push_str(&format!( + "use crate::generated::dto::{}::{};\n", + message_type_snake, message_type + )); + } else { + content.push_str(&format!( + "use crate::generated::models::{}::{};\n", + message_type_snake, message_type + )); + } + } + content.push_str(&format!( - "pub type {}Message = {};\n\n", - ws.name, rust_type + "#[derive(Debug, Clone, Serialize, Deserialize)]\n" )); + content.push_str(&format!("pub struct {}Message\n", ws.name)); + content.push_str("{\n"); + content.push_str(&format!(" pub data: {},\n", rust_type)); + content.push_str(" pub timestamp: chrono::DateTime,\n"); + content.push_str("}\n\n"); } content.push_str(&format!( @@ -474,14 +568,25 @@ pub fn generate_middlewares(schema: &Schema, output_dir: &Path) -> Result<()> { } } - let handlers_dir = output_dir.join("handlers/middlewares"); + let middlewares_dir = output_dir.join("middlewares"); + fs::create_dir_all(&middlewares_dir)?; + 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); + let handler_path = middlewares_dir.join(&file_name); if !handler_path.exists() { let content = generate_middleware_stub(&mw_name); - fs::write(handler_path, content)?; + fs::write(&handler_path, content).map_err(|e| { + crate::error::CodegenError::Io(std::io::Error::new( + e.kind(), + format!( + "Failed to write middleware handler {}: {}", + handler_path.display(), + e + ) + )) + })?; } } @@ -651,9 +756,14 @@ pub fn generate_lib_rs(schema: &Schema, output_dir: &Path) -> Result<()> { // Generate handlers module declarations let handlers_dir = output_dir.join("handlers"); - if handlers_dir.join("api").exists() || handlers_dir.join("events").exists() { + let middlewares_dir = output_dir.join("middlewares"); + if handlers_dir.join("api").exists() || handlers_dir.join("events").exists() || middlewares_dir.exists() { main_lib_content.push_str("pub mod handlers;\n\n"); } + + if middlewares_dir.exists() { + main_lib_content.push_str("pub mod middlewares;\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"); @@ -711,6 +821,7 @@ pub fn generate_lib_rs(schema: &Schema, output_dir: &Path) -> Result<()> { fn generate_handlers_mod(schema: &Schema, output_dir: &Path) -> Result<()> { let handlers_dir = output_dir.join("handlers"); + let middlewares_dir = output_dir.join("middlewares"); let mut content = String::new(); content.push_str("// Handler module declarations\n\n"); @@ -723,6 +834,10 @@ fn generate_handlers_mod(schema: &Schema, output_dir: &Path) -> Result<()> { content.push_str("pub mod events;\n"); } + if handlers_dir.join("websockets").exists() { + content.push_str("pub mod websockets;\n"); + } + fs::write(handlers_dir.join("mod.rs"), content)?; if handlers_dir.join("api").exists() { @@ -756,6 +871,60 @@ fn generate_handlers_mod(schema: &Schema, output_dir: &Path) -> Result<()> { fs::write(handlers_dir.join("events").join("mod.rs"), events_mod)?; } + if handlers_dir.join("websockets").exists() { + let mut websockets_mod = String::new(); + websockets_mod.push_str("// WebSocket handler modules\n\n"); + + for ws in &schema.websockets { + let mut all_handlers = std::collections::HashSet::new(); + for handler in &ws.on_connect { + all_handlers.insert(handler.clone()); + } + for handler in &ws.on_message { + all_handlers.insert(handler.clone()); + } + for handler in &ws.on_disconnect { + all_handlers.insert(handler.clone()); + } + + for handler in all_handlers { + let handler_file = handlers_dir.join("websockets").join(format!("{}.rs", handler)); + if handler_file.exists() { + websockets_mod.push_str(&format!("pub mod {};\n", handler)); + } + } + } + + fs::write(handlers_dir.join("websockets").join("mod.rs"), websockets_mod)?; + } + + if middlewares_dir.exists() { + let mut middlewares_mod = String::new(); + middlewares_mod.push_str("// Middleware handler modules\n\n"); + + 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()); + } + } + + for mw_name in middleware_names { + let mw_snake = templates::to_snake_case(&mw_name); + let handler_file = middlewares_dir.join(format!("{}.rs", mw_snake)); + if handler_file.exists() { + middlewares_mod.push_str(&format!("pub mod {};\n", mw_snake)); + } + } + + fs::write(middlewares_dir.join("mod.rs"), middlewares_mod)?; + } + Ok(()) } @@ -850,6 +1019,62 @@ fn generate_handlers_registration(schema: &Schema, output_dir: &Path) -> Result< } } + let websockets_handlers_dir = output_dir.join("handlers/websockets"); + for ws in &schema.websockets { + for handler in &ws.on_connect { + let handler_file = websockets_handlers_dir.join(format!("{}.rs", handler)); + if handler_file.exists() { + content.push_str(&format!( + "use crate::handlers::websockets::{}::{};\n", + handler, handler + )); + } + } + for handler in &ws.on_message { + let handler_file = websockets_handlers_dir.join(format!("{}.rs", handler)); + if handler_file.exists() { + content.push_str(&format!( + "use crate::handlers::websockets::{}::{};\n", + handler, handler + )); + } + } + for handler in &ws.on_disconnect { + let handler_file = websockets_handlers_dir.join(format!("{}.rs", handler)); + if handler_file.exists() { + content.push_str(&format!( + "use crate::handlers::websockets::{}::{};\n", + handler, handler + )); + } + } + } + + let middlewares_dir = output_dir.join("middlewares"); + 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()); + } + } + + for mw_name in &middleware_names { + let mw_snake = templates::to_snake_case(mw_name); + let handler_file = middlewares_dir.join(format!("{}.rs", mw_snake)); + if handler_file.exists() { + let handler_fn_name = format!("{}_middleware", mw_snake); + content.push_str(&format!( + "use crate::middlewares::{}::{};\n", + mw_snake, handler_fn_name + )); + } + } + 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"); @@ -915,6 +1140,218 @@ fn generate_handlers_registration(schema: &Schema, output_dir: &Path) -> Result< } } + let websockets_handlers_dir = output_dir.join("handlers/websockets"); + for ws in &schema.websockets { + let ws_module = templates::to_snake_case(&ws.name); + + for handler in &ws.on_connect { + let handler_file = websockets_handlers_dir.join(format!("{}.rs", handler)); + if handler_file.exists() { + content.push_str(&format!( + " // Register WebSocket connect handler: {}\n", + handler + )); + content.push_str(&format!( + " runtime.register_handler(\n" + )); + content.push_str(&format!( + " \"{}\".to_string(),\n", + handler + )); + content.push_str(&format!( + " |ctx: HandlerContext| async move {{\n" + )); + content.push_str(&format!( + " // Parse connection from context\n" + )); + content.push_str(&format!( + " let connection: crate::generated::websockets::{}::{}Connection = serde_json::from_value(ctx.payload.clone())?;\n", + ws_module, ws.name + )); + content.push_str(&format!( + " let mut state = crate::generated::state::State::new(&ctx.handler_name);\n" + )); + content.push_str(&format!( + " let result = {}(connection, &mut state).await?;\n", + handler + )); + content.push_str(&format!( + " Ok(result)\n" + )); + content.push_str(&format!( + " }}\n" + )); + content.push_str(&format!( + " ).await;\n" + )); + content.push_str(&format!( + " info!(\"Registered WebSocket connect handler: {}\");\n", + handler + )); + } + } + + for handler in &ws.on_message { + let handler_file = websockets_handlers_dir.join(format!("{}.rs", handler)); + if handler_file.exists() { + content.push_str(&format!( + " // Register WebSocket message handler: {}\n", + handler + )); + content.push_str(&format!( + " runtime.register_handler(\n" + )); + content.push_str(&format!( + " \"{}\".to_string(),\n", + handler + )); + content.push_str(&format!( + " |ctx: HandlerContext| async move {{\n" + )); + content.push_str(&format!( + " // Parse message and connection from context payload\n" + )); + content.push_str(&format!( + " let payload: serde_json::Value = ctx.payload.clone();\n" + )); + if ws.message.is_some() { + content.push_str(&format!( + " let message: crate::generated::websockets::{}::{}Message = serde_json::from_value(payload.get(\"message\").cloned().unwrap_or(serde_json::json!({{}})))?;\n", + ws_module, ws.name + )); + } + content.push_str(&format!( + " let connection: crate::generated::websockets::{}::{}Connection = serde_json::from_value(payload.get(\"connection\").cloned().unwrap_or(serde_json::json!({{}})))?;\n", + ws_module, ws.name + )); + content.push_str(&format!( + " let mut state = crate::generated::state::State::new(&ctx.handler_name);\n" + )); + if ws.message.is_some() { + content.push_str(&format!( + " let result = {}(message, connection, &mut state).await?;\n", + handler + )); + } else { + content.push_str(&format!( + " let result = {}(connection, &mut state).await?;\n", + handler + )); + } + content.push_str(&format!( + " Ok(result)\n" + )); + content.push_str(&format!( + " }}\n" + )); + content.push_str(&format!( + " ).await;\n" + )); + content.push_str(&format!( + " info!(\"Registered WebSocket message handler: {}\");\n", + handler + )); + } + } + + for handler in &ws.on_disconnect { + let handler_file = websockets_handlers_dir.join(format!("{}.rs", handler)); + if handler_file.exists() { + content.push_str(&format!( + " // Register WebSocket disconnect handler: {}\n", + handler + )); + content.push_str(&format!( + " runtime.register_handler(\n" + )); + content.push_str(&format!( + " \"{}\".to_string(),\n", + handler + )); + content.push_str(&format!( + " |ctx: HandlerContext| async move {{\n" + )); + content.push_str(&format!( + " // Parse connection from context\n" + )); + content.push_str(&format!( + " let connection: crate::generated::websockets::{}::{}Connection = serde_json::from_value(ctx.payload.clone())?;\n", + ws_module, ws.name + )); + content.push_str(&format!( + " let result = {}(connection).await?;\n", + handler + )); + content.push_str(&format!( + " Ok(result)\n" + )); + content.push_str(&format!( + " }}\n" + )); + content.push_str(&format!( + " ).await;\n" + )); + content.push_str(&format!( + " info!(\"Registered WebSocket disconnect handler: {}\");\n", + handler + )); + } + } + } + + + let middlewares_dir = output_dir.join("middlewares"); + 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()); + } + } + + for mw_name in middleware_names { + let mw_snake = templates::to_snake_case(&mw_name); + let handler_file = middlewares_dir.join(format!("{}.rs", mw_snake)); + if handler_file.exists() { + let handler_fn_name = format!("{}_middleware", mw_snake); + content.push_str(&format!( + " // Register middleware handler: {}\n", + mw_name + )); + content.push_str(&format!( + " runtime.register_handler(\n" + )); + content.push_str(&format!( + " \"{}\".to_string(),\n", + mw_snake + )); + content.push_str(&format!( + " |ctx: HandlerContext| async move {{\n" + )); + content.push_str(&format!( + " let mut state = crate::generated::state::State::new(&ctx.handler_name);\n" + )); + content.push_str(&format!( + " {}(ctx, &mut state).await\n", + handler_fn_name + )); + content.push_str(&format!( + " }}\n" + )); + content.push_str(&format!( + " ).await;\n" + )); + content.push_str(&format!( + " info!(\"Registered middleware handler: {}\");\n", + mw_snake + )); + } + } + content.push_str(" Ok::<(), rohas_runtime::RuntimeError>(())\n"); content.push_str(" })?;\n"); content.push_str(" Ok(())\n"); diff --git a/crates/rohas-dev-server/src/rust_compiler.rs b/crates/rohas-dev-server/src/rust_compiler.rs index 681f549..add1ad3 100644 --- a/crates/rohas-dev-server/src/rust_compiler.rs +++ b/crates/rohas-dev-server/src/rust_compiler.rs @@ -51,6 +51,8 @@ impl RustCompiler { info!("Building Rust project in release mode: {}", self.project_root().display()); + self.ensure_dylib_config()?; + let package_name = self.get_package_name()?; let project_name = package_name.replace('-', "_"); @@ -200,7 +202,49 @@ impl RustCompiler { } if !dylib_path.exists() { - return Err(anyhow::anyhow!("Dylib was not created at expected path: {}", dylib_path.display())); + let target_profile_dir = self.project_root().join("target").join("release"); + let mut diagnostic_msg = format!( + "Dylib was not created at expected path: {}\n", + dylib_path.display() + ); + + if target_profile_dir.exists() { + diagnostic_msg.push_str(&format!("Target directory exists: {}\n", target_profile_dir.display())); + + if let Ok(entries) = fs::read_dir(&target_profile_dir) { + let mut dylib_files = Vec::new(); + for entry in entries.flatten() { + let path = entry.path(); + if let Some(ext) = path.extension() { + if ext == "dylib" || ext == "so" || ext == "dll" { + if let Some(name) = path.file_name().and_then(|n| n.to_str()) { + dylib_files.push(name.to_string()); + } + } + } + } + + if !dylib_files.is_empty() { + diagnostic_msg.push_str(&format!("Found {} dylib files in target/release:\n", dylib_files.len())); + for file in &dylib_files { + diagnostic_msg.push_str(&format!(" - {}\n", file)); + } + diagnostic_msg.push_str(&format!("\nExpected: {}\n", dylib_path.file_name().and_then(|n| n.to_str()).unwrap_or("unknown"))); + diagnostic_msg.push_str("This might indicate a mismatch between the package/lib name in Cargo.toml and the expected dylib name."); + } else { + diagnostic_msg.push_str("No dylib files found in target/release directory.\n"); + diagnostic_msg.push_str("This might indicate:\n"); + diagnostic_msg.push_str(" 1. The build failed to create a dylib\n"); + diagnostic_msg.push_str(" 2. The crate-type is not set to [\"dylib\"] in Cargo.toml\n"); + diagnostic_msg.push_str(" 3. The build output is in a different location\n"); + } + } + } else { + diagnostic_msg.push_str(&format!("Target directory does not exist: {}\n", target_profile_dir.display())); + diagnostic_msg.push_str("The build may have failed or not completed."); + } + + return Err(anyhow::anyhow!("{}", diagnostic_msg)); } let dylib_hash = Self::compute_file_hash(&dylib_path)?; @@ -345,6 +389,37 @@ impl RustCompiler { Ok(name) } + fn get_lib_name(&self) -> anyhow::Result { + use std::fs; + + let cargo_toml = self.project_root().join("Cargo.toml"); + let contents = fs::read_to_string(&cargo_toml)?; + + let mut in_lib_section = false; + for line in contents.lines() { + let line = line.trim(); + if line.starts_with("[lib]") { + in_lib_section = true; + continue; + } + if line.starts_with('[') && line.ends_with(']') { + in_lib_section = false; + continue; + } + if in_lib_section && 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()); + } + } + } + } + } + + self.get_package_name() + } + pub fn get_library_path(&self) -> anyhow::Result { let profile = if cfg!(debug_assertions) { "debug" @@ -357,8 +432,9 @@ impl RustCompiler { 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('-', "_"); + // Use lib name if specified, otherwise fall back to package name + let lib_name = self.get_lib_name()?; + let project_name = lib_name.replace('-', "_"); #[cfg(target_os = "macos")] let dylib_name = format!("lib{}.dylib", project_name); @@ -372,6 +448,40 @@ impl RustCompiler { Ok(target_dir.join(profile).join(&dylib_name)) } + fn ensure_dylib_config(&self) -> anyhow::Result<()> { + use std::fs; + + let cargo_toml = self.project_root().join("Cargo.toml"); + if !cargo_toml.exists() { + return Err(anyhow::anyhow!("Cargo.toml not found at: {}", cargo_toml.display())); + } + + let cargo_content = 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) + }; + fs::write(&cargo_toml, updated_content)?; + info!("Updated Cargo.toml to build as dylib"); + } + + Ok(()) + } + fn compute_file_hash(path: &PathBuf) -> anyhow::Result<[u8; 32]> { use sha2::{Sha256, Digest}; let mut file = fs::File::open(path)?; diff --git a/examples/rust-example/Cargo.lock b/examples/rust-example/Cargo.lock index df31524..3a45ccd 100644 --- a/examples/rust-example/Cargo.lock +++ b/examples/rust-example/Cargo.lock @@ -1074,6 +1074,8 @@ dependencies = [ [[package]] name = "rohas-codegen" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53259f8f13193d8811e334d594d98cd1ac8ba74fc29adb540a6926f75e324c2a" dependencies = [ "anyhow", "rohas-parser", @@ -1087,6 +1089,8 @@ dependencies = [ [[package]] name = "rohas-parser" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff82225e47bb505727050b06ac6a27657f568f278294c3af0645caf7702fc80e" dependencies = [ "anyhow", "pest", @@ -1100,6 +1104,8 @@ dependencies = [ [[package]] name = "rohas-runtime" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492f5035be2cb3e62e59c715e742c0fd153b567e6fa188a6a52d4591911aae6" dependencies = [ "anyhow", "async-trait", diff --git a/examples/rust-example/Cargo.toml b/examples/rust-example/Cargo.toml index 8da104d..baaac10 100644 --- a/examples/rust-example/Cargo.toml +++ b/examples/rust-example/Cargo.toml @@ -10,7 +10,7 @@ name = "rust_example" path = "src/lib.rs" [dependencies] -rohas-runtime = { path = "../../crates/rohas-runtime" } +rohas-runtime = { version = "*" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tokio = { version = "1.0", features = ["full"] } diff --git a/examples/rust-example/schema/api/user_api.ro b/examples/rust-example/schema/api/user_api.ro index 6f5897f..789b742 100644 --- a/examples/rust-example/schema/api/user_api.ro +++ b/examples/rust-example/schema/api/user_api.ro @@ -17,3 +17,19 @@ api HelloWorld { path: "/hello-world" response: String } + +input WebSocketMessage { + type: String + payload: Json +} + +ws HelloWorldWS { + path: "/ws/hello" + message: WebSocketMessage + onConnect: [on_connect_handler] + onMessage: [on_message_handler] + onDisconnect: [on_disconnect_handler] + triggers: [UserCreated] + broadcast: true + middlewares: [auth, rate_limit, request_logging] +} diff --git a/examples/rust-example/src/handlers/middlewares/auth.rs b/examples/rust-example/src/handlers/middlewares/auth.rs new file mode 100644 index 0000000..659bb5b --- /dev/null +++ b/examples/rust-example/src/handlers/middlewares/auth.rs @@ -0,0 +1,13 @@ +use rohas_runtime::{HandlerContext, HandlerResult, Result}; +use crate::generated::state::State; + +/// High-performance Rust middleware. +pub async fn auth_middleware( + ctx: HandlerContext, + state: &mut State, +) -> Result { + // TODO: Implement middleware logic + // Return Ok to continue, Err to abort + tracing::info!("Middleware auth executed"); + Ok(HandlerResult::success(serde_json::json!({}), 0)) +} diff --git a/examples/rust-example/src/handlers/middlewares/rate_limit.rs b/examples/rust-example/src/handlers/middlewares/rate_limit.rs new file mode 100644 index 0000000..09a9909 --- /dev/null +++ b/examples/rust-example/src/handlers/middlewares/rate_limit.rs @@ -0,0 +1,13 @@ +use rohas_runtime::{HandlerContext, HandlerResult, Result}; +use crate::generated::state::State; + +/// High-performance Rust middleware. +pub async fn rate_limit_middleware( + ctx: HandlerContext, + state: &mut State, +) -> Result { + // TODO: Implement middleware logic + // Return Ok to continue, Err to abort + tracing::info!("Middleware rate_limit executed"); + Ok(HandlerResult::success(serde_json::json!({}), 0)) +} diff --git a/examples/rust-example/src/handlers/middlewares/request_logging.rs b/examples/rust-example/src/handlers/middlewares/request_logging.rs new file mode 100644 index 0000000..bf5829a --- /dev/null +++ b/examples/rust-example/src/handlers/middlewares/request_logging.rs @@ -0,0 +1,13 @@ +use rohas_runtime::{HandlerContext, HandlerResult, Result}; +use crate::generated::state::State; + +/// High-performance Rust middleware. +pub async fn request_logging_middleware( + ctx: HandlerContext, + state: &mut State, +) -> Result { + // TODO: Implement middleware logic + // Return Ok to continue, Err to abort + tracing::info!("Middleware request_logging executed"); + 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 index a06481c..c834b48 100644 --- a/examples/rust-example/src/handlers/mod.rs +++ b/examples/rust-example/src/handlers/mod.rs @@ -2,3 +2,4 @@ pub mod api; pub mod events; +pub mod websockets; diff --git a/examples/rust-example/src/handlers/websockets/mod.rs b/examples/rust-example/src/handlers/websockets/mod.rs new file mode 100644 index 0000000..16fe0b4 --- /dev/null +++ b/examples/rust-example/src/handlers/websockets/mod.rs @@ -0,0 +1,5 @@ +// WebSocket handler modules + +pub mod on_connect_handler; +pub mod on_message_handler; +pub mod on_disconnect_handler; diff --git a/examples/rust-example/src/handlers/websockets/on_connect_handler.rs b/examples/rust-example/src/handlers/websockets/on_connect_handler.rs new file mode 100644 index 0000000..3ed8310 --- /dev/null +++ b/examples/rust-example/src/handlers/websockets/on_connect_handler.rs @@ -0,0 +1,13 @@ +use crate::generated::websockets::hello_world_w_s::HelloWorldWSConnection; +use crate::generated::websockets::hello_world_w_s::HelloWorldWSMessage; +use rohas_runtime::{HandlerContext, HandlerResult, Result}; +use crate::generated::state::State; + +/// Rust WebSocket connect handler. +pub async fn on_connect_handler( + connection: HelloWorldWSConnection, + state: &mut State, +) -> Result { + tracing::info!("WebSocket connect handler: {:?}", connection); + Ok(HandlerResult::success(serde_json::json!({}), 0)) +} diff --git a/examples/rust-example/src/handlers/websockets/on_disconnect_handler.rs b/examples/rust-example/src/handlers/websockets/on_disconnect_handler.rs new file mode 100644 index 0000000..4489634 --- /dev/null +++ b/examples/rust-example/src/handlers/websockets/on_disconnect_handler.rs @@ -0,0 +1,12 @@ +use crate::generated::websockets::hello_world_w_s::HelloWorldWSConnection; +use crate::generated::websockets::hello_world_w_s::HelloWorldWSMessage; +use rohas_runtime::{HandlerContext, HandlerResult, Result}; +use crate::generated::state::State; + +/// Rust WebSocket disconnect handler. +pub async fn on_disconnect_handler( + connection: HelloWorldWSConnection, +) -> Result { + tracing::info!("WebSocket disconnect handler: {:?}", connection); + Ok(HandlerResult::success(serde_json::json!({}), 0)) +} diff --git a/examples/rust-example/src/handlers/websockets/on_message_handler.rs b/examples/rust-example/src/handlers/websockets/on_message_handler.rs new file mode 100644 index 0000000..c2b3f53 --- /dev/null +++ b/examples/rust-example/src/handlers/websockets/on_message_handler.rs @@ -0,0 +1,14 @@ +use crate::generated::websockets::hello_world_w_s::HelloWorldWSConnection; +use crate::generated::websockets::hello_world_w_s::HelloWorldWSMessage; +use rohas_runtime::{HandlerContext, HandlerResult, Result}; +use crate::generated::state::State; + +/// Rust WebSocket message handler. +pub async fn on_message_handler( + message: HelloWorldWSMessage, + connection: HelloWorldWSConnection, + state: &mut State, +) -> Result { + tracing::info!("WebSocket message handler: {:?}", connection); + Ok(HandlerResult::success(serde_json::json!({}), 0)) +} diff --git a/examples/rust-example/src/lib.rs b/examples/rust-example/src/lib.rs index f3a3442..a96df90 100644 --- a/examples/rust-example/src/lib.rs +++ b/examples/rust-example/src/lib.rs @@ -9,6 +9,8 @@ pub use generated::*; pub mod handlers; +pub mod middlewares; + /// 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. @@ -22,30 +24,30 @@ pub async fn init_handlers(runtime: std::sync::Arc) #[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 } } diff --git a/examples/rust-example/src/middlewares/auth.rs b/examples/rust-example/src/middlewares/auth.rs new file mode 100644 index 0000000..659bb5b --- /dev/null +++ b/examples/rust-example/src/middlewares/auth.rs @@ -0,0 +1,13 @@ +use rohas_runtime::{HandlerContext, HandlerResult, Result}; +use crate::generated::state::State; + +/// High-performance Rust middleware. +pub async fn auth_middleware( + ctx: HandlerContext, + state: &mut State, +) -> Result { + // TODO: Implement middleware logic + // Return Ok to continue, Err to abort + tracing::info!("Middleware auth executed"); + Ok(HandlerResult::success(serde_json::json!({}), 0)) +} diff --git a/examples/rust-example/src/middlewares/mod.rs b/examples/rust-example/src/middlewares/mod.rs new file mode 100644 index 0000000..4244908 --- /dev/null +++ b/examples/rust-example/src/middlewares/mod.rs @@ -0,0 +1,5 @@ +// Middleware handler modules + +pub mod auth; +pub mod rate_limit; +pub mod request_logging; diff --git a/examples/rust-example/src/middlewares/rate_limit.rs b/examples/rust-example/src/middlewares/rate_limit.rs new file mode 100644 index 0000000..09a9909 --- /dev/null +++ b/examples/rust-example/src/middlewares/rate_limit.rs @@ -0,0 +1,13 @@ +use rohas_runtime::{HandlerContext, HandlerResult, Result}; +use crate::generated::state::State; + +/// High-performance Rust middleware. +pub async fn rate_limit_middleware( + ctx: HandlerContext, + state: &mut State, +) -> Result { + // TODO: Implement middleware logic + // Return Ok to continue, Err to abort + tracing::info!("Middleware rate_limit executed"); + Ok(HandlerResult::success(serde_json::json!({}), 0)) +} diff --git a/examples/rust-example/src/middlewares/request_logging.rs b/examples/rust-example/src/middlewares/request_logging.rs new file mode 100644 index 0000000..bf5829a --- /dev/null +++ b/examples/rust-example/src/middlewares/request_logging.rs @@ -0,0 +1,13 @@ +use rohas_runtime::{HandlerContext, HandlerResult, Result}; +use crate::generated::state::State; + +/// High-performance Rust middleware. +pub async fn request_logging_middleware( + ctx: HandlerContext, + state: &mut State, +) -> Result { + // TODO: Implement middleware logic + // Return Ok to continue, Err to abort + tracing::info!("Middleware request_logging executed"); + Ok(HandlerResult::success(serde_json::json!({}), 0)) +}