From 64c63a92e5fb780c016afddeb3046b388f8be3f3 Mon Sep 17 00:00:00 2001 From: Tunay Engin Date: Sat, 10 Jan 2026 15:37:35 +0300 Subject: [PATCH 1/7] Refactor path params and optimize JSON handling Replaces HashMap usage with PathParams for request path parameters throughout core and middleware modules. Switches JSON parsing and serialization to use simd-json for improved performance and adds buffer pre-allocation for responses. Implements CORS middleware logic in rustapi-extras, handling preflight and actual requests with correct headers. Updates tests to use PathParams. --- .gitignore | 2 + crates/rustapi-core/src/app.rs | 12 +- crates/rustapi-core/src/extract.rs | 37 +++-- .../rustapi-core/src/middleware/body_limit.rs | 6 +- crates/rustapi-core/src/middleware/layer.rs | 4 +- .../rustapi-core/src/middleware/request_id.rs | 5 +- .../src/middleware/tracing_layer.rs | 3 +- crates/rustapi-extras/src/cors/mod.rs | 136 +++++++++++++++++- 8 files changed, 180 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 1d69e7c..25af064 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ assets/myadam.jpg .github/copilot-instructions.md docs/UPDATE_SUMMARIES.md +assets/cb7d0daf60d7675081996d81393e2ae5.jpg +assets/b9c93c1cd427d8f50e68dbd11ed2b000.jpg diff --git a/crates/rustapi-core/src/app.rs b/crates/rustapi-core/src/app.rs index 9a12275..102c72b 100644 --- a/crates/rustapi-core/src/app.rs +++ b/crates/rustapi-core/src/app.rs @@ -266,17 +266,19 @@ impl RustApi { entry.insert_boxed_with_operation(method_enum, route.handler, route.operation); } - let route_count = by_path + #[cfg(feature = "tracing")] + let route_count: usize = by_path .values() .map(|mr| mr.allowed_methods().len()) - .sum::(); + .sum(); + #[cfg(feature = "tracing")] let path_count = by_path.len(); for (path, method_router) in by_path { self = self.route(&path, method_router); } - tracing::info!( + crate::trace_info!( paths = path_count, routes = route_count, "Auto-registered routes" @@ -887,12 +889,12 @@ impl Default for RustApi { mod tests { use super::RustApi; use crate::extract::{FromRequestParts, State}; + use crate::path_params::PathParams; use crate::request::Request; use crate::router::{get, post, Router}; use bytes::Bytes; use http::Method; use proptest::prelude::*; - use std::collections::HashMap; #[test] fn state_is_available_via_extractor() { @@ -906,7 +908,7 @@ mod tests { .unwrap(); let (parts, _) = req.into_parts(); - let request = Request::new(parts, Bytes::new(), router.state_ref(), HashMap::new()); + let request = Request::new(parts, Bytes::new(), router.state_ref(), PathParams::new()); let State(value) = State::::from_request_parts(&request).unwrap(); assert_eq!(value, 123u32); } diff --git a/crates/rustapi-core/src/extract.rs b/crates/rustapi-core/src/extract.rs index 75e22e6..3f0a244 100644 --- a/crates/rustapi-core/src/extract.rs +++ b/crates/rustapi-core/src/extract.rs @@ -55,6 +55,7 @@ //! in any order. use crate::error::{ApiError, Result}; +use crate::json; use crate::request::Request; use crate::response::IntoResponse; use bytes::Bytes; @@ -116,7 +117,8 @@ impl FromRequest for Json { .take_body() .ok_or_else(|| ApiError::internal("Body already consumed"))?; - let value: T = serde_json::from_slice(&body)?; + // Use simd-json accelerated parsing when available (2-4x faster) + let value: T = json::from_slice(&body)?; Ok(Json(value)) } } @@ -141,10 +143,15 @@ impl From for Json { } } +/// Default pre-allocation size for JSON response buffers (256 bytes) +/// This covers most small to medium JSON responses without reallocation. +const JSON_RESPONSE_INITIAL_CAPACITY: usize = 256; + // IntoResponse for Json - allows using Json as a return type impl IntoResponse for Json { fn into_response(self) -> crate::response::Response { - match serde_json::to_vec(&self.0) { + // Use pre-allocated buffer to reduce allocations + match json::to_vec_with_capacity(&self.0, JSON_RESPONSE_INITIAL_CAPACITY) { Ok(body) => http::Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "application/json") @@ -199,12 +206,12 @@ impl ValidatedJson { impl FromRequest for ValidatedJson { async fn from_request(req: &mut Request) -> Result { - // First, deserialize the JSON body + // First, deserialize the JSON body using simd-json when available let body = req .take_body() .ok_or_else(|| ApiError::internal("Body already consumed"))?; - let value: T = serde_json::from_slice(&body)?; + let value: T = json::from_slice(&body)?; // Then, validate it if let Err(validation_error) = rustapi_validate::Validate::validate(&value) { @@ -778,10 +785,17 @@ impl Schema<'a>> OperationModifier for Json { } } -// Path - Placeholder for path params +// Path - Path parameters are automatically extracted from route patterns +// The add_path_params_to_operation function in app.rs handles OpenAPI documentation +// based on the {param} syntax in route paths (e.g., "/users/{id}") impl OperationModifier for Path { fn update_operation(_op: &mut Operation) { - // TODO: Implement path param extraction + // Path parameters are automatically documented by add_path_params_to_operation + // in app.rs based on the route pattern. No additional implementation needed here. + // + // For typed path params, the schema type defaults to "string" but will be + // inferred from the actual type T when more sophisticated type introspection + // is implemented. } } @@ -885,6 +899,7 @@ impl Schema<'a>> ResponseModifier for Json { #[cfg(test)] mod tests { use super::*; + use crate::path_params::PathParams; use bytes::Bytes; use http::{Extensions, Method}; use proptest::prelude::*; @@ -912,7 +927,7 @@ mod tests { parts, Bytes::new(), Arc::new(Extensions::new()), - HashMap::new(), + PathParams::new(), ) } @@ -933,7 +948,7 @@ mod tests { parts, Bytes::new(), Arc::new(Extensions::new()), - HashMap::new(), + PathParams::new(), ) } @@ -1109,7 +1124,7 @@ mod tests { parts, Bytes::new(), Arc::new(Extensions::new()), - HashMap::new(), + PathParams::new(), ); let extracted = ClientIp::extract_with_config(&request, trust_proxy) @@ -1171,7 +1186,7 @@ mod tests { parts, Bytes::new(), Arc::new(Extensions::new()), - HashMap::new(), + PathParams::new(), ); let result = Extension::::from_request_parts(&request); @@ -1271,7 +1286,7 @@ mod tests { parts, Bytes::new(), Arc::new(Extensions::new()), - HashMap::new(), + PathParams::new(), ); let ip = ClientIp::extract_with_config(&request, false).unwrap(); diff --git a/crates/rustapi-core/src/middleware/body_limit.rs b/crates/rustapi-core/src/middleware/body_limit.rs index c7979b6..0e9098e 100644 --- a/crates/rustapi-core/src/middleware/body_limit.rs +++ b/crates/rustapi-core/src/middleware/body_limit.rs @@ -121,11 +121,11 @@ impl MiddlewareLayer for BodyLimitLayer { #[cfg(test)] mod tests { use super::*; + use crate::path_params::PathParams; use crate::request::Request; use bytes::Bytes; use http::{Extensions, Method}; use proptest::prelude::*; - use std::collections::HashMap; use std::sync::Arc; /// Create a test request with the given body @@ -139,7 +139,7 @@ mod tests { let req = builder.body(()).unwrap(); let (parts, _) = req.into_parts(); - Request::new(parts, body, Arc::new(Extensions::new()), HashMap::new()) + Request::new(parts, body, Arc::new(Extensions::new()), PathParams::new()) } /// Create a test request without Content-Length header @@ -150,7 +150,7 @@ mod tests { let req = builder.body(()).unwrap(); let (parts, _) = req.into_parts(); - Request::new(parts, body, Arc::new(Extensions::new()), HashMap::new()) + Request::new(parts, body, Arc::new(Extensions::new()), PathParams::new()) } /// Create a simple handler that returns 200 OK diff --git a/crates/rustapi-core/src/middleware/layer.rs b/crates/rustapi-core/src/middleware/layer.rs index 1cabe71..49dcc86 100644 --- a/crates/rustapi-core/src/middleware/layer.rs +++ b/crates/rustapi-core/src/middleware/layer.rs @@ -192,13 +192,13 @@ impl Service for NextService { #[cfg(test)] mod tests { use super::*; + use crate::path_params::PathParams; use crate::request::Request; use crate::response::Response; use bytes::Bytes; use http::{Extensions, Method, StatusCode}; use proptest::prelude::*; use proptest::test_runner::TestCaseError; - use std::collections::HashMap; /// Create a test request with the given method and path fn create_test_request(method: Method, path: &str) -> Request { @@ -212,7 +212,7 @@ mod tests { parts, Bytes::new(), Arc::new(Extensions::new()), - HashMap::new(), + PathParams::new(), ) } diff --git a/crates/rustapi-core/src/middleware/request_id.rs b/crates/rustapi-core/src/middleware/request_id.rs index 0fed5e5..b0a00c0 100644 --- a/crates/rustapi-core/src/middleware/request_id.rs +++ b/crates/rustapi-core/src/middleware/request_id.rs @@ -167,11 +167,12 @@ fn generate_uuid() -> String { mod tests { use super::*; use crate::middleware::layer::{BoxedNext, LayerStack}; + use crate::path_params::PathParams; use bytes::Bytes; use http::{Extensions, Method, StatusCode}; use proptest::prelude::*; use proptest::test_runner::TestCaseError; - use std::collections::{HashMap, HashSet}; + use std::collections::HashSet; use std::sync::Arc; /// Create a test request with the given method and path @@ -186,7 +187,7 @@ mod tests { parts, Bytes::new(), Arc::new(Extensions::new()), - HashMap::new(), + PathParams::new(), ) } diff --git a/crates/rustapi-core/src/middleware/tracing_layer.rs b/crates/rustapi-core/src/middleware/tracing_layer.rs index b4b54ab..9c1c816 100644 --- a/crates/rustapi-core/src/middleware/tracing_layer.rs +++ b/crates/rustapi-core/src/middleware/tracing_layer.rs @@ -204,6 +204,7 @@ mod tests { use super::*; use crate::middleware::layer::{BoxedNext, LayerStack}; use crate::middleware::request_id::RequestIdLayer; + use crate::path_params::PathParams; use bytes::Bytes; use http::{Extensions, Method, StatusCode}; use proptest::prelude::*; @@ -224,7 +225,7 @@ mod tests { parts, Bytes::new(), Arc::new(Extensions::new()), - HashMap::new(), + PathParams::new(), ) } diff --git a/crates/rustapi-extras/src/cors/mod.rs b/crates/rustapi-extras/src/cors/mod.rs index 310b0e2..fb585ea 100644 --- a/crates/rustapi-extras/src/cors/mod.rs +++ b/crates/rustapi-extras/src/cors/mod.rs @@ -14,7 +14,13 @@ //! .allow_credentials(true); //! ``` -use http::Method; +use bytes::Bytes; +use http::{header, Method, StatusCode}; +use http_body_util::Full; +use rustapi_core::middleware::{BoxedNext, MiddlewareLayer}; +use rustapi_core::{Request, Response}; +use std::future::Future; +use std::pin::Pin; use std::time::Duration; /// Specifies which origins are allowed for CORS requests. @@ -161,4 +167,132 @@ impl CorsLayer { pub fn max_age_duration(&self) -> Option { self.max_age } + + /// Build the Access-Control-Allow-Methods header value. + fn methods_header_value(&self) -> String { + self.methods + .iter() + .map(|m| m.as_str()) + .collect::>() + .join(", ") + } + + /// Build the Access-Control-Allow-Headers header value. + fn headers_header_value(&self) -> String { + if self.headers.is_empty() { + "Content-Type, Authorization".to_string() + } else { + self.headers.join(", ") + } + } +} + +impl MiddlewareLayer for CorsLayer { + fn call( + &self, + req: Request, + next: BoxedNext, + ) -> Pin + Send + 'static>> { + let origins = self.origins.clone(); + let methods = self.methods_header_value(); + let headers = self.headers_header_value(); + let credentials = self.credentials; + let max_age = self.max_age; + let is_any_origin = matches!(origins, AllowedOrigins::Any); + + // Extract origin from request + let origin = req + .headers() + .get(header::ORIGIN) + .and_then(|v| v.to_str().ok()) + .map(String::from); + + // Check if this is a preflight request + let is_preflight = req.method() == Method::OPTIONS + && req.headers().contains_key(header::ACCESS_CONTROL_REQUEST_METHOD); + + // Clone self for origin check + let is_origin_allowed = origin + .as_ref() + .map(|o| { + match &origins { + AllowedOrigins::Any => true, + AllowedOrigins::List(list) => list.iter().any(|allowed| allowed == o), + } + }) + .unwrap_or(false); + + Box::pin(async move { + // Handle preflight request + if is_preflight { + let mut response = http::Response::builder() + .status(StatusCode::NO_CONTENT) + .body(Full::new(Bytes::new())) + .unwrap(); + + let headers_mut = response.headers_mut(); + + // Set Allow-Origin + if let Some(ref origin) = origin { + if is_origin_allowed { + if is_any_origin && !credentials { + headers_mut.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*".parse().unwrap()); + } else { + headers_mut.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.parse().unwrap()); + } + } + } + + // Set Allow-Methods + headers_mut.insert(header::ACCESS_CONTROL_ALLOW_METHODS, methods.parse().unwrap()); + + // Set Allow-Headers + headers_mut.insert(header::ACCESS_CONTROL_ALLOW_HEADERS, headers.parse().unwrap()); + + // Set Allow-Credentials + if credentials { + headers_mut.insert(header::ACCESS_CONTROL_ALLOW_CREDENTIALS, "true".parse().unwrap()); + } + + // Set Max-Age + if let Some(max_age) = max_age { + headers_mut.insert(header::ACCESS_CONTROL_MAX_AGE, max_age.as_secs().to_string().parse().unwrap()); + } + + return response; + } + + // Process the actual request + let mut response = next(req).await; + + // Add CORS headers to the response + if let Some(ref origin) = origin { + if is_origin_allowed { + let headers_mut = response.headers_mut(); + + if is_any_origin && !credentials { + headers_mut.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*".parse().unwrap()); + } else { + headers_mut.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.parse().unwrap()); + } + + if credentials { + headers_mut.insert(header::ACCESS_CONTROL_ALLOW_CREDENTIALS, "true".parse().unwrap()); + } + + // Expose headers that the browser can access + headers_mut.insert( + header::ACCESS_CONTROL_EXPOSE_HEADERS, + "Content-Length, Content-Type".parse().unwrap(), + ); + } + } + + response + }) + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } } From 00fee2da4ef12d67bf55118e24683fc228ebb2ba Mon Sep 17 00:00:00 2001 From: Tunay Engin Date: Sat, 10 Jan 2026 15:49:30 +0300 Subject: [PATCH 2/7] Refactor to remove PathParams in favor of HashMap Replaces usage of the PathParams struct with std::collections::HashMap throughout the codebase, including tests. Also removes dependency on the custom json module in favor of serde_json for JSON serialization and deserialization. Updates logging to use tracing directly instead of crate::trace_info! macro. --- crates/rustapi-core/src/app.rs | 8 +++--- crates/rustapi-core/src/extract.rs | 25 ++++++------------- .../rustapi-core/src/middleware/body_limit.rs | 6 ++--- crates/rustapi-core/src/middleware/layer.rs | 4 +-- .../rustapi-core/src/middleware/request_id.rs | 5 ++-- .../src/middleware/tracing_layer.rs | 3 +-- 6 files changed, 19 insertions(+), 32 deletions(-) diff --git a/crates/rustapi-core/src/app.rs b/crates/rustapi-core/src/app.rs index 102c72b..c5bf45f 100644 --- a/crates/rustapi-core/src/app.rs +++ b/crates/rustapi-core/src/app.rs @@ -266,19 +266,17 @@ impl RustApi { entry.insert_boxed_with_operation(method_enum, route.handler, route.operation); } - #[cfg(feature = "tracing")] let route_count: usize = by_path .values() .map(|mr| mr.allowed_methods().len()) .sum(); - #[cfg(feature = "tracing")] let path_count = by_path.len(); for (path, method_router) in by_path { self = self.route(&path, method_router); } - crate::trace_info!( + tracing::info!( paths = path_count, routes = route_count, "Auto-registered routes" @@ -889,12 +887,12 @@ impl Default for RustApi { mod tests { use super::RustApi; use crate::extract::{FromRequestParts, State}; - use crate::path_params::PathParams; use crate::request::Request; use crate::router::{get, post, Router}; use bytes::Bytes; use http::Method; use proptest::prelude::*; + use std::collections::HashMap; #[test] fn state_is_available_via_extractor() { @@ -908,7 +906,7 @@ mod tests { .unwrap(); let (parts, _) = req.into_parts(); - let request = Request::new(parts, Bytes::new(), router.state_ref(), PathParams::new()); + let request = Request::new(parts, Bytes::new(), router.state_ref(), HashMap::new()); let State(value) = State::::from_request_parts(&request).unwrap(); assert_eq!(value, 123u32); } diff --git a/crates/rustapi-core/src/extract.rs b/crates/rustapi-core/src/extract.rs index 3f0a244..c4aef83 100644 --- a/crates/rustapi-core/src/extract.rs +++ b/crates/rustapi-core/src/extract.rs @@ -55,7 +55,6 @@ //! in any order. use crate::error::{ApiError, Result}; -use crate::json; use crate::request::Request; use crate::response::IntoResponse; use bytes::Bytes; @@ -117,8 +116,7 @@ impl FromRequest for Json { .take_body() .ok_or_else(|| ApiError::internal("Body already consumed"))?; - // Use simd-json accelerated parsing when available (2-4x faster) - let value: T = json::from_slice(&body)?; + let value: T = serde_json::from_slice(&body)?; Ok(Json(value)) } } @@ -143,15 +141,10 @@ impl From for Json { } } -/// Default pre-allocation size for JSON response buffers (256 bytes) -/// This covers most small to medium JSON responses without reallocation. -const JSON_RESPONSE_INITIAL_CAPACITY: usize = 256; - // IntoResponse for Json - allows using Json as a return type impl IntoResponse for Json { fn into_response(self) -> crate::response::Response { - // Use pre-allocated buffer to reduce allocations - match json::to_vec_with_capacity(&self.0, JSON_RESPONSE_INITIAL_CAPACITY) { + match serde_json::to_vec(&self.0) { Ok(body) => http::Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "application/json") @@ -206,12 +199,11 @@ impl ValidatedJson { impl FromRequest for ValidatedJson { async fn from_request(req: &mut Request) -> Result { - // First, deserialize the JSON body using simd-json when available let body = req .take_body() .ok_or_else(|| ApiError::internal("Body already consumed"))?; - let value: T = json::from_slice(&body)?; + let value: T = serde_json::from_slice(&body)?; // Then, validate it if let Err(validation_error) = rustapi_validate::Validate::validate(&value) { @@ -899,7 +891,6 @@ impl Schema<'a>> ResponseModifier for Json { #[cfg(test)] mod tests { use super::*; - use crate::path_params::PathParams; use bytes::Bytes; use http::{Extensions, Method}; use proptest::prelude::*; @@ -927,7 +918,7 @@ mod tests { parts, Bytes::new(), Arc::new(Extensions::new()), - PathParams::new(), + HashMap::new(), ) } @@ -948,7 +939,7 @@ mod tests { parts, Bytes::new(), Arc::new(Extensions::new()), - PathParams::new(), + HashMap::new(), ) } @@ -1124,7 +1115,7 @@ mod tests { parts, Bytes::new(), Arc::new(Extensions::new()), - PathParams::new(), + HashMap::new(), ); let extracted = ClientIp::extract_with_config(&request, trust_proxy) @@ -1186,7 +1177,7 @@ mod tests { parts, Bytes::new(), Arc::new(Extensions::new()), - PathParams::new(), + HashMap::new(), ); let result = Extension::::from_request_parts(&request); @@ -1286,7 +1277,7 @@ mod tests { parts, Bytes::new(), Arc::new(Extensions::new()), - PathParams::new(), + HashMap::new(), ); let ip = ClientIp::extract_with_config(&request, false).unwrap(); diff --git a/crates/rustapi-core/src/middleware/body_limit.rs b/crates/rustapi-core/src/middleware/body_limit.rs index 0e9098e..c7979b6 100644 --- a/crates/rustapi-core/src/middleware/body_limit.rs +++ b/crates/rustapi-core/src/middleware/body_limit.rs @@ -121,11 +121,11 @@ impl MiddlewareLayer for BodyLimitLayer { #[cfg(test)] mod tests { use super::*; - use crate::path_params::PathParams; use crate::request::Request; use bytes::Bytes; use http::{Extensions, Method}; use proptest::prelude::*; + use std::collections::HashMap; use std::sync::Arc; /// Create a test request with the given body @@ -139,7 +139,7 @@ mod tests { let req = builder.body(()).unwrap(); let (parts, _) = req.into_parts(); - Request::new(parts, body, Arc::new(Extensions::new()), PathParams::new()) + Request::new(parts, body, Arc::new(Extensions::new()), HashMap::new()) } /// Create a test request without Content-Length header @@ -150,7 +150,7 @@ mod tests { let req = builder.body(()).unwrap(); let (parts, _) = req.into_parts(); - Request::new(parts, body, Arc::new(Extensions::new()), PathParams::new()) + Request::new(parts, body, Arc::new(Extensions::new()), HashMap::new()) } /// Create a simple handler that returns 200 OK diff --git a/crates/rustapi-core/src/middleware/layer.rs b/crates/rustapi-core/src/middleware/layer.rs index 49dcc86..1cabe71 100644 --- a/crates/rustapi-core/src/middleware/layer.rs +++ b/crates/rustapi-core/src/middleware/layer.rs @@ -192,13 +192,13 @@ impl Service for NextService { #[cfg(test)] mod tests { use super::*; - use crate::path_params::PathParams; use crate::request::Request; use crate::response::Response; use bytes::Bytes; use http::{Extensions, Method, StatusCode}; use proptest::prelude::*; use proptest::test_runner::TestCaseError; + use std::collections::HashMap; /// Create a test request with the given method and path fn create_test_request(method: Method, path: &str) -> Request { @@ -212,7 +212,7 @@ mod tests { parts, Bytes::new(), Arc::new(Extensions::new()), - PathParams::new(), + HashMap::new(), ) } diff --git a/crates/rustapi-core/src/middleware/request_id.rs b/crates/rustapi-core/src/middleware/request_id.rs index b0a00c0..0fed5e5 100644 --- a/crates/rustapi-core/src/middleware/request_id.rs +++ b/crates/rustapi-core/src/middleware/request_id.rs @@ -167,12 +167,11 @@ fn generate_uuid() -> String { mod tests { use super::*; use crate::middleware::layer::{BoxedNext, LayerStack}; - use crate::path_params::PathParams; use bytes::Bytes; use http::{Extensions, Method, StatusCode}; use proptest::prelude::*; use proptest::test_runner::TestCaseError; - use std::collections::HashSet; + use std::collections::{HashMap, HashSet}; use std::sync::Arc; /// Create a test request with the given method and path @@ -187,7 +186,7 @@ mod tests { parts, Bytes::new(), Arc::new(Extensions::new()), - PathParams::new(), + HashMap::new(), ) } diff --git a/crates/rustapi-core/src/middleware/tracing_layer.rs b/crates/rustapi-core/src/middleware/tracing_layer.rs index 9c1c816..b4b54ab 100644 --- a/crates/rustapi-core/src/middleware/tracing_layer.rs +++ b/crates/rustapi-core/src/middleware/tracing_layer.rs @@ -204,7 +204,6 @@ mod tests { use super::*; use crate::middleware::layer::{BoxedNext, LayerStack}; use crate::middleware::request_id::RequestIdLayer; - use crate::path_params::PathParams; use bytes::Bytes; use http::{Extensions, Method, StatusCode}; use proptest::prelude::*; @@ -225,7 +224,7 @@ mod tests { parts, Bytes::new(), Arc::new(Extensions::new()), - PathParams::new(), + HashMap::new(), ) } From 5d574cefb3915903534d29e45263b73dfa8a1b20 Mon Sep 17 00:00:00 2001 From: Tunay Engin Date: Sat, 10 Jan 2026 15:52:37 +0300 Subject: [PATCH 3/7] Refactor CORS middleware for improved readability Reformatted the CorsLayer middleware logic to improve code readability by expanding chained method calls and aligning insertions for response headers. No functional changes were made. Minor whitespace cleanup in extract.rs and app.rs. --- crates/rustapi-core/src/app.rs | 5 +-- crates/rustapi-core/src/extract.rs | 2 +- crates/rustapi-extras/src/cors/mod.rs | 51 +++++++++++++++++++-------- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/crates/rustapi-core/src/app.rs b/crates/rustapi-core/src/app.rs index c5bf45f..c394ae2 100644 --- a/crates/rustapi-core/src/app.rs +++ b/crates/rustapi-core/src/app.rs @@ -266,10 +266,7 @@ impl RustApi { entry.insert_boxed_with_operation(method_enum, route.handler, route.operation); } - let route_count: usize = by_path - .values() - .map(|mr| mr.allowed_methods().len()) - .sum(); + let route_count: usize = by_path.values().map(|mr| mr.allowed_methods().len()).sum(); let path_count = by_path.len(); for (path, method_router) in by_path { diff --git a/crates/rustapi-core/src/extract.rs b/crates/rustapi-core/src/extract.rs index c4aef83..6d8810b 100644 --- a/crates/rustapi-core/src/extract.rs +++ b/crates/rustapi-core/src/extract.rs @@ -784,7 +784,7 @@ impl OperationModifier for Path { fn update_operation(_op: &mut Operation) { // Path parameters are automatically documented by add_path_params_to_operation // in app.rs based on the route pattern. No additional implementation needed here. - // + // // For typed path params, the schema type defaults to "string" but will be // inferred from the actual type T when more sophisticated type introspection // is implemented. diff --git a/crates/rustapi-extras/src/cors/mod.rs b/crates/rustapi-extras/src/cors/mod.rs index fb585ea..9252bca 100644 --- a/crates/rustapi-extras/src/cors/mod.rs +++ b/crates/rustapi-extras/src/cors/mod.rs @@ -209,16 +209,16 @@ impl MiddlewareLayer for CorsLayer { // Check if this is a preflight request let is_preflight = req.method() == Method::OPTIONS - && req.headers().contains_key(header::ACCESS_CONTROL_REQUEST_METHOD); + && req + .headers() + .contains_key(header::ACCESS_CONTROL_REQUEST_METHOD); // Clone self for origin check let is_origin_allowed = origin .as_ref() - .map(|o| { - match &origins { - AllowedOrigins::Any => true, - AllowedOrigins::List(list) => list.iter().any(|allowed| allowed == o), - } + .map(|o| match &origins { + AllowedOrigins::Any => true, + AllowedOrigins::List(list) => list.iter().any(|allowed| allowed == o), }) .unwrap_or(false); @@ -236,27 +236,43 @@ impl MiddlewareLayer for CorsLayer { if let Some(ref origin) = origin { if is_origin_allowed { if is_any_origin && !credentials { - headers_mut.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*".parse().unwrap()); + headers_mut + .insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*".parse().unwrap()); } else { - headers_mut.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.parse().unwrap()); + headers_mut.insert( + header::ACCESS_CONTROL_ALLOW_ORIGIN, + origin.parse().unwrap(), + ); } } } // Set Allow-Methods - headers_mut.insert(header::ACCESS_CONTROL_ALLOW_METHODS, methods.parse().unwrap()); + headers_mut.insert( + header::ACCESS_CONTROL_ALLOW_METHODS, + methods.parse().unwrap(), + ); // Set Allow-Headers - headers_mut.insert(header::ACCESS_CONTROL_ALLOW_HEADERS, headers.parse().unwrap()); + headers_mut.insert( + header::ACCESS_CONTROL_ALLOW_HEADERS, + headers.parse().unwrap(), + ); // Set Allow-Credentials if credentials { - headers_mut.insert(header::ACCESS_CONTROL_ALLOW_CREDENTIALS, "true".parse().unwrap()); + headers_mut.insert( + header::ACCESS_CONTROL_ALLOW_CREDENTIALS, + "true".parse().unwrap(), + ); } // Set Max-Age if let Some(max_age) = max_age { - headers_mut.insert(header::ACCESS_CONTROL_MAX_AGE, max_age.as_secs().to_string().parse().unwrap()); + headers_mut.insert( + header::ACCESS_CONTROL_MAX_AGE, + max_age.as_secs().to_string().parse().unwrap(), + ); } return response; @@ -271,13 +287,18 @@ impl MiddlewareLayer for CorsLayer { let headers_mut = response.headers_mut(); if is_any_origin && !credentials { - headers_mut.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*".parse().unwrap()); + headers_mut + .insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*".parse().unwrap()); } else { - headers_mut.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.parse().unwrap()); + headers_mut + .insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.parse().unwrap()); } if credentials { - headers_mut.insert(header::ACCESS_CONTROL_ALLOW_CREDENTIALS, "true".parse().unwrap()); + headers_mut.insert( + header::ACCESS_CONTROL_ALLOW_CREDENTIALS, + "true".parse().unwrap(), + ); } // Expose headers that the browser can access From b14f29cb623116d5cb2b0415239633f99d6dc018 Mon Sep 17 00:00:00 2001 From: Tunay Engin Date: Sat, 10 Jan 2026 16:03:44 +0300 Subject: [PATCH 4/7] ci: add disk cleanup step to fix runner space issues --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d89be24..7a2e6ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,6 +69,14 @@ jobs: name: Build runs-on: ubuntu-latest steps: + - name: Free disk space + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/share/boost + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + df -h + - uses: actions/checkout@v4 - name: Install Rust From 5274871d6722ae3b9612f46b472ff94b13056926 Mon Sep 17 00:00:00 2001 From: Tunay Engin Date: Sat, 10 Jan 2026 16:09:02 +0300 Subject: [PATCH 5/7] chore: bump version to 0.1.8 --- CHANGELOG.md | 12 ++++++++++++ Cargo.toml | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81be027..711192e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.1.8] - 2026-01-10 + +### Added +- **CORS middleware**: `CorsLayer` with full `MiddlewareLayer` trait implementation + - Support for `CorsLayer::permissive()` and custom configuration + - Proper preflight request handling + - Origin validation and credential support + +### Fixed +- Fixed missing `MiddlewareLayer` implementation for `CorsLayer` +- Fixed CI build issues with GitHub Actions runner disk space + ## [0.1.4] - 2026-01-03 ### Added diff --git a/Cargo.toml b/Cargo.toml index 35e074a..c57f108 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ members = [ ] [workspace.package] -version = "0.1.7" +version = "0.1.8" edition = "2021" authors = ["RustAPI Contributors"] license = "MIT OR Apache-2.0" From ffee5325984ff3ce2f467dcab8f816ce0b19fda8 Mon Sep 17 00:00:00 2001 From: Tunay Engin Date: Sat, 10 Jan 2026 16:44:50 +0300 Subject: [PATCH 6/7] Add CORS test example project Introduces a new example 'cors-test' to demonstrate CORS, rate limiting, and middleware usage with rustapi-rs. Updates workspace members in Cargo.toml and bumps rustapi-related crate versions to 0.1.8. --- Cargo.lock | 33 ++++++++++++++++++++++----------- Cargo.toml | 1 + examples/cors-test/Cargo.toml | 11 +++++++++++ examples/cors-test/src/main.rs | 21 +++++++++++++++++++++ 4 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 examples/cors-test/Cargo.toml create mode 100644 examples/cors-test/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 1b509c0..c456bcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -293,7 +293,7 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cargo-rustapi" -version = "0.1.7" +version = "0.1.8" dependencies = [ "anyhow", "assert_cmd", @@ -520,6 +520,17 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cors-test" +version = "0.1.0" +dependencies = [ + "rustapi-macros", + "rustapi-rs", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -2590,7 +2601,7 @@ dependencies = [ [[package]] name = "rustapi-core" -version = "0.1.7" +version = "0.1.8" dependencies = [ "base64 0.22.1", "brotli 6.0.0", @@ -2626,7 +2637,7 @@ dependencies = [ [[package]] name = "rustapi-extras" -version = "0.1.7" +version = "0.1.8" dependencies = [ "bytes", "cookie", @@ -2653,7 +2664,7 @@ dependencies = [ [[package]] name = "rustapi-macros" -version = "0.1.7" +version = "0.1.8" dependencies = [ "proc-macro2", "quote", @@ -2662,7 +2673,7 @@ dependencies = [ [[package]] name = "rustapi-openapi" -version = "0.1.7" +version = "0.1.8" dependencies = [ "bytes", "http", @@ -2674,7 +2685,7 @@ dependencies = [ [[package]] name = "rustapi-rs" -version = "0.1.7" +version = "0.1.8" dependencies = [ "rustapi-core", "rustapi-extras", @@ -2693,7 +2704,7 @@ dependencies = [ [[package]] name = "rustapi-toon" -version = "0.1.7" +version = "0.1.8" dependencies = [ "bytes", "futures-util", @@ -2711,7 +2722,7 @@ dependencies = [ [[package]] name = "rustapi-validate" -version = "0.1.7" +version = "0.1.8" dependencies = [ "http", "serde", @@ -2723,7 +2734,7 @@ dependencies = [ [[package]] name = "rustapi-view" -version = "0.1.7" +version = "0.1.8" dependencies = [ "bytes", "http", @@ -2740,7 +2751,7 @@ dependencies = [ [[package]] name = "rustapi-ws" -version = "0.1.7" +version = "0.1.8" dependencies = [ "base64 0.22.1", "bytes", @@ -3724,7 +3735,7 @@ dependencies = [ [[package]] name = "toon-bench" -version = "0.1.7" +version = "0.1.8" dependencies = [ "criterion", "serde", diff --git a/Cargo.toml b/Cargo.toml index c57f108..bd81a97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ members = [ # "examples/graphql-api", # TODO: Needs API updates "examples/microservices", "examples/middleware-chain", + "examples/cors-test", "benches/toon_bench", ] diff --git a/examples/cors-test/Cargo.toml b/examples/cors-test/Cargo.toml new file mode 100644 index 0000000..d314a6b --- /dev/null +++ b/examples/cors-test/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "cors-test" +version = "0.1.0" +edition = "2021" + +[dependencies] +rustapi-rs = { path = "../../crates/rustapi-rs", features = ["cors", "rate-limit"] } +rustapi-macros = { path = "../../crates/rustapi-macros" } +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/examples/cors-test/src/main.rs b/examples/cors-test/src/main.rs new file mode 100644 index 0000000..544177c --- /dev/null +++ b/examples/cors-test/src/main.rs @@ -0,0 +1,21 @@ +use rustapi_rs::prelude::*; +use std::time::Duration; + +async fn hello() -> &'static str { + "Hello from CORS-enabled API!" +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("🚀 Testing CorsLayer with the exact user configuration..."); + println!("✅ If this compiles, CorsLayer works!"); + + RustApi::new() + .route("/", get(hello)) + .layer(CorsLayer::permissive()) + .layer(RequestIdLayer::new()) + .layer(TracingLayer::new()) + .layer(RateLimitLayer::new(100, Duration::from_secs(60))) + .run("127.0.0.1:3030") + .await +} From 255d9560716138c408ce66ec5b470fdfd813ec4b Mon Sep 17 00:00:00 2001 From: Tunay Engin Date: Sat, 10 Jan 2026 17:15:29 +0300 Subject: [PATCH 7/7] ci: add disk cleanup to all jobs to prevent space issues --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a2e6ee..0db5c09 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,14 @@ jobs: name: Test runs-on: ubuntu-latest steps: + - name: Free disk space + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/share/boost + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + df -h + - uses: actions/checkout@v4 - name: Install Rust @@ -41,6 +49,14 @@ jobs: name: Lint runs-on: ubuntu-latest steps: + - name: Free disk space + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/share/boost + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + df -h + - uses: actions/checkout@v4 - name: Install Rust @@ -106,6 +122,14 @@ jobs: name: Documentation runs-on: ubuntu-latest steps: + - name: Free disk space + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/share/boost + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + df -h + - uses: actions/checkout@v4 - name: Install Rust