From c78d2f4a02b13cb611f56f2da22d99ee28ce0f17 Mon Sep 17 00:00:00 2001 From: Kilerd Chan Date: Thu, 5 Feb 2026 02:33:25 +0800 Subject: [PATCH 1/3] Fix Path parameter generation for simple types in OpenAPI When using Path with simple types like Uuid (e.g., Path(user_id): Path), the OpenAPI parameter was not being generated correctly because the implementation only handled structs with properties. This fix updates the Path ParameterProvider implementation to handle both: 1. Structs with properties (existing behavior) 2. Simple types like Uuid, String, i32, etc. (new behavior) For simple types, it now extracts the parameter name from the URL pattern and generates the appropriate OpenAPI parameter. Added comprehensive tests to verify the fix works for: - Path (simple type) - Path<(Uuid,)> (tuple syntax) - Path<(Uuid, String)> (multiple parameters) --- examples/path_param_test/Cargo.toml | 14 +++ .../configurations/application.toml | 5 + examples/path_param_test/main.rs | 117 ++++++++++++++++++ gotcha/src/openapi/schematic.rs | 20 ++- gotcha/tests/test_path_params.rs | 72 +++++++++++ test_path_params.rs | 68 ++++++++++ 6 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 examples/path_param_test/Cargo.toml create mode 100644 examples/path_param_test/configurations/application.toml create mode 100644 examples/path_param_test/main.rs create mode 100644 gotcha/tests/test_path_params.rs create mode 100644 test_path_params.rs diff --git a/examples/path_param_test/Cargo.toml b/examples/path_param_test/Cargo.toml new file mode 100644 index 0000000..4b19187 --- /dev/null +++ b/examples/path_param_test/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "path_param_test" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "path_param_test" +path = "main.rs" + +[dependencies] +gotcha = { path = "../../gotcha", features = ["openapi"] } +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +uuid = { version = "1", features = ["serde"] } \ No newline at end of file diff --git a/examples/path_param_test/configurations/application.toml b/examples/path_param_test/configurations/application.toml new file mode 100644 index 0000000..e9fa572 --- /dev/null +++ b/examples/path_param_test/configurations/application.toml @@ -0,0 +1,5 @@ +[basic] +host = "0.0.0.0" +port = 3456 + +[application] diff --git a/examples/path_param_test/main.rs b/examples/path_param_test/main.rs new file mode 100644 index 0000000..8a80970 --- /dev/null +++ b/examples/path_param_test/main.rs @@ -0,0 +1,117 @@ +use gotcha::prelude::*; +use gotcha::GotchaService; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Clone)] +struct App; + +#[derive(Clone)] +struct AppState {} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +struct AppConfig {} + +#[derive(Debug, Serialize, Deserialize, Schematic)] +struct UsageQuery { + start: Option, + end: Option, +} + +#[derive(Debug, Serialize, Deserialize, Schematic)] +struct UsageStats { + user_id: Uuid, + total_requests: u64, + total_tokens: u64, + total_cost: f64, + by_model: Vec, + by_provider: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Schematic)] +struct ModelStats { + model: String, + requests: u64, + prompt_tokens: u64, + completion_tokens: u64, + total_tokens: u64, + cost: f64, +} + +#[derive(Debug, Serialize, Deserialize, Schematic)] +struct ProviderStats { + provider: String, + requests: u64, + total_tokens: u64, + cost: f64, +} + +#[gotcha::api] +async fn get_usage_stats( + Path(user_id): Path, + Query(_query): Query, +) -> Result, StatusCode> { + // Mock implementation + Ok(Json(UsageStats { + user_id, + total_requests: 100, + total_tokens: 50000, + total_cost: 25.0, + by_model: vec![ + ModelStats { + model: "gpt-4".to_string(), + requests: 50, + prompt_tokens: 20000, + completion_tokens: 5000, + total_tokens: 25000, + cost: 20.0, + }, + ], + by_provider: vec![ + ProviderStats { + provider: "openai".to_string(), + requests: 100, + total_tokens: 50000, + cost: 25.0, + }, + ], + })) +} + +#[gotcha::api] +async fn get_user_by_id( + Path(id): Path, +) -> String { + format!("User ID: {}", id) +} + +// Test with tuple syntax (this should already work) +#[gotcha::api] +async fn get_user_tuple( + Path((id,)): Path<(Uuid,)>, +) -> String { + format!("User ID from tuple: {}", id) +} + +impl GotchaApp for App { + type State = AppState; + type Config = AppConfig; + + async fn state(&self, _config: &ConfigWrapper) -> Result> { + Ok(AppState {}) + } + + fn routes(&self, router: GotchaRouter>) -> GotchaRouter> { + router + .get("/admin/users/:user_id/usage", get_usage_stats) + .get("/users/:id", get_user_by_id) + .get("/users-tuple/:id", get_user_tuple) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let app = App; + app.run().await?; + Ok(()) +} \ No newline at end of file diff --git a/gotcha/src/openapi/schematic.rs b/gotcha/src/openapi/schematic.rs index 9eba705..cea9b35 100644 --- a/gotcha/src/openapi/schematic.rs +++ b/gotcha/src/openapi/schematic.rs @@ -413,10 +413,13 @@ impl ParameterProvider for Path<(T1, T2)> { } impl ParameterProvider for Path { - fn generate(_url: String) -> Either, RequestBody> { + fn generate(url: String) -> Either, RequestBody> { let mut ret = vec![]; let mut schema = T::generate_schema(); + + // Check if this is a struct with properties or a simple type if let Some(mut properties) = schema.schema.extras.remove("properties") { + // Case 1: Struct with properties - each property becomes a path parameter if let Some(properties) = properties.as_object_mut() { properties.iter_mut().for_each(|(key, value)| { let schema = serde_json::from_value(value.clone()).unwrap(); @@ -424,6 +427,21 @@ impl ParameterProvider for Path { ret.push(param); }) } + } else { + // Case 2: Simple type like Uuid - extract parameter name from URL + let pattern = regex::Regex::new(r":([^/]+)").unwrap(); + let param_names_in_path: Vec = pattern.captures_iter(&url).map(|digits| digits.get(1).unwrap().as_str().to_string()).collect(); + + if let Some(param_name) = param_names_in_path.first() { + let param = build_param( + param_name.clone(), + ParameterIn::Path, + T::required(), + T::generate_schema().schema, + T::doc(), + ); + ret.push(param); + } } Either::Left(ret) } diff --git a/gotcha/tests/test_path_params.rs b/gotcha/tests/test_path_params.rs new file mode 100644 index 0000000..29c4004 --- /dev/null +++ b/gotcha/tests/test_path_params.rs @@ -0,0 +1,72 @@ +#[cfg(feature = "openapi")] +#[test] +fn test_path_uuid_parameter() { + use either::Either; + use gotcha::{ParameterProvider, Path}; + use uuid::Uuid; + + // Test that Path generates a parameter + let url = "/users/:user_id".to_string(); + let result = as ParameterProvider>::generate(url); + + match result { + Either::Left(params) => { + assert_eq!(params.len(), 1, "Should generate exactly one parameter"); + assert_eq!(params[0].name, "user_id", "Parameter name should be 'user_id'"); + assert_eq!(params[0].required, Some(true), "Path parameter should be required"); + + // Verify it's a path parameter + use oas::ParameterIn; + assert!(matches!(params[0]._in, ParameterIn::Path), "Should be a path parameter"); + } + Either::Right(_) => { + panic!("Path should generate parameters, not a request body"); + } + } +} + +#[cfg(feature = "openapi")] +#[test] +fn test_path_tuple_parameter() { + use either::Either; + use gotcha::{ParameterProvider, Path}; + use uuid::Uuid; + + // Test that Path<(Uuid,)> also works (this should already work) + let url = "/users/:user_id".to_string(); + let result = as ParameterProvider>::generate(url); + + match result { + Either::Left(params) => { + assert_eq!(params.len(), 1, "Should generate exactly one parameter"); + assert_eq!(params[0].name, "user_id", "Parameter name should be 'user_id'"); + assert_eq!(params[0].required, Some(true), "Path parameter should be required"); + } + Either::Right(_) => { + panic!("Path<(Uuid,)> should generate parameters, not a request body"); + } + } +} + +#[cfg(feature = "openapi")] +#[test] +fn test_multiple_path_params() { + use either::Either; + use gotcha::{ParameterProvider, Path}; + use uuid::Uuid; + + // Test multiple parameters with Path<(Uuid, String)> + let url = "/users/:user_id/posts/:post_id".to_string(); + let result = as ParameterProvider>::generate(url); + + match result { + Either::Left(params) => { + assert_eq!(params.len(), 2, "Should generate two parameters"); + assert_eq!(params[0].name, "user_id", "First parameter name should be 'user_id'"); + assert_eq!(params[1].name, "post_id", "Second parameter name should be 'post_id'"); + } + Either::Right(_) => { + panic!("Path<(Uuid, String)> should generate parameters, not a request body"); + } + } +} \ No newline at end of file diff --git a/test_path_params.rs b/test_path_params.rs new file mode 100644 index 0000000..90ea0c1 --- /dev/null +++ b/test_path_params.rs @@ -0,0 +1,68 @@ +// Test script to verify Path parameter fix +// This tests that Path parameters are properly included in OpenAPI JSON + +use gotcha::prelude::*; +use uuid::Uuid; +use serde::{Deserialize, Serialize}; + +#[derive(Schematic, Serialize, Deserialize)] +struct UserResponse { + id: Uuid, + name: String, +} + +#[gotcha::api] +async fn get_user_by_id( + Path(id): Path, +) -> Json { + Json(UserResponse { + id, + name: "Test User".to_string(), + }) +} + +#[tokio::main] +async fn main() { + // Test 1: Simple type (Uuid) path parameter + let url = "/users/:id".to_string(); + let params = as gotcha::ParameterProvider>::generate(url); + + match params { + gotcha::openapi::Either::Left(params) => { + println!("✅ Path generates parameters:"); + for param in ¶ms { + println!(" - Parameter name: {}", param.name); + println!(" Location: {:?}", param._in); + println!(" Required: {}", param.required); + } + + if params.is_empty() { + println!("❌ ERROR: No parameters generated for Path"); + } else { + println!("✅ SUCCESS: Path correctly generates {} parameter(s)", params.len()); + } + } + gotcha::openapi::Either::Right(_) => { + println!("❌ ERROR: Path should generate parameters, not request body"); + } + } + + // Test 2: Tuple syntax (this should already work) + let url2 = "/users/:id".to_string(); + let params2 = as gotcha::ParameterProvider>::generate(url2); + + match params2 { + gotcha::openapi::Either::Left(params) => { + println!("\n✅ Path<(Uuid,)> generates parameters:"); + for param in ¶ms { + println!(" - Parameter name: {}", param.name); + println!(" Location: {:?}", param._in); + } + } + gotcha::openapi::Either::Right(_) => { + println!("❌ ERROR: Path<(Uuid,)> should generate parameters"); + } + } + + println!("\nTest completed!"); +} \ No newline at end of file From 0c6248707f41322b4069c44ac286fdd9cd9eb386 Mon Sep 17 00:00:00 2001 From: Kilerd Chan Date: Thu, 5 Feb 2026 02:52:08 +0800 Subject: [PATCH 2/3] Remove temporary test file --- test_path_params.rs | 68 --------------------------------------------- 1 file changed, 68 deletions(-) delete mode 100644 test_path_params.rs diff --git a/test_path_params.rs b/test_path_params.rs deleted file mode 100644 index 90ea0c1..0000000 --- a/test_path_params.rs +++ /dev/null @@ -1,68 +0,0 @@ -// Test script to verify Path parameter fix -// This tests that Path parameters are properly included in OpenAPI JSON - -use gotcha::prelude::*; -use uuid::Uuid; -use serde::{Deserialize, Serialize}; - -#[derive(Schematic, Serialize, Deserialize)] -struct UserResponse { - id: Uuid, - name: String, -} - -#[gotcha::api] -async fn get_user_by_id( - Path(id): Path, -) -> Json { - Json(UserResponse { - id, - name: "Test User".to_string(), - }) -} - -#[tokio::main] -async fn main() { - // Test 1: Simple type (Uuid) path parameter - let url = "/users/:id".to_string(); - let params = as gotcha::ParameterProvider>::generate(url); - - match params { - gotcha::openapi::Either::Left(params) => { - println!("✅ Path generates parameters:"); - for param in ¶ms { - println!(" - Parameter name: {}", param.name); - println!(" Location: {:?}", param._in); - println!(" Required: {}", param.required); - } - - if params.is_empty() { - println!("❌ ERROR: No parameters generated for Path"); - } else { - println!("✅ SUCCESS: Path correctly generates {} parameter(s)", params.len()); - } - } - gotcha::openapi::Either::Right(_) => { - println!("❌ ERROR: Path should generate parameters, not request body"); - } - } - - // Test 2: Tuple syntax (this should already work) - let url2 = "/users/:id".to_string(); - let params2 = as gotcha::ParameterProvider>::generate(url2); - - match params2 { - gotcha::openapi::Either::Left(params) => { - println!("\n✅ Path<(Uuid,)> generates parameters:"); - for param in ¶ms { - println!(" - Parameter name: {}", param.name); - println!(" Location: {:?}", param._in); - } - } - gotcha::openapi::Either::Right(_) => { - println!("❌ ERROR: Path<(Uuid,)> should generate parameters"); - } - } - - println!("\nTest completed!"); -} \ No newline at end of file From b73b0e319b77474f63a5680e93d7340e3be03458 Mon Sep 17 00:00:00 2001 From: Kilerd Chan Date: Thu, 5 Feb 2026 02:53:14 +0800 Subject: [PATCH 3/3] Remove path_param_test example - keep only tests The example was mainly for verification purposes. The test suite in gotcha/tests/test_path_params.rs is sufficient to validate the fix. --- examples/path_param_test/Cargo.toml | 14 --- .../configurations/application.toml | 5 - examples/path_param_test/main.rs | 117 ------------------ 3 files changed, 136 deletions(-) delete mode 100644 examples/path_param_test/Cargo.toml delete mode 100644 examples/path_param_test/configurations/application.toml delete mode 100644 examples/path_param_test/main.rs diff --git a/examples/path_param_test/Cargo.toml b/examples/path_param_test/Cargo.toml deleted file mode 100644 index 4b19187..0000000 --- a/examples/path_param_test/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "path_param_test" -version = "0.1.0" -edition = "2021" - -[[bin]] -name = "path_param_test" -path = "main.rs" - -[dependencies] -gotcha = { path = "../../gotcha", features = ["openapi"] } -tokio = { version = "1", features = ["full"] } -serde = { version = "1", features = ["derive"] } -uuid = { version = "1", features = ["serde"] } \ No newline at end of file diff --git a/examples/path_param_test/configurations/application.toml b/examples/path_param_test/configurations/application.toml deleted file mode 100644 index e9fa572..0000000 --- a/examples/path_param_test/configurations/application.toml +++ /dev/null @@ -1,5 +0,0 @@ -[basic] -host = "0.0.0.0" -port = 3456 - -[application] diff --git a/examples/path_param_test/main.rs b/examples/path_param_test/main.rs deleted file mode 100644 index 8a80970..0000000 --- a/examples/path_param_test/main.rs +++ /dev/null @@ -1,117 +0,0 @@ -use gotcha::prelude::*; -use gotcha::GotchaService; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -#[derive(Clone)] -struct App; - -#[derive(Clone)] -struct AppState {} - -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -struct AppConfig {} - -#[derive(Debug, Serialize, Deserialize, Schematic)] -struct UsageQuery { - start: Option, - end: Option, -} - -#[derive(Debug, Serialize, Deserialize, Schematic)] -struct UsageStats { - user_id: Uuid, - total_requests: u64, - total_tokens: u64, - total_cost: f64, - by_model: Vec, - by_provider: Vec, -} - -#[derive(Debug, Serialize, Deserialize, Schematic)] -struct ModelStats { - model: String, - requests: u64, - prompt_tokens: u64, - completion_tokens: u64, - total_tokens: u64, - cost: f64, -} - -#[derive(Debug, Serialize, Deserialize, Schematic)] -struct ProviderStats { - provider: String, - requests: u64, - total_tokens: u64, - cost: f64, -} - -#[gotcha::api] -async fn get_usage_stats( - Path(user_id): Path, - Query(_query): Query, -) -> Result, StatusCode> { - // Mock implementation - Ok(Json(UsageStats { - user_id, - total_requests: 100, - total_tokens: 50000, - total_cost: 25.0, - by_model: vec![ - ModelStats { - model: "gpt-4".to_string(), - requests: 50, - prompt_tokens: 20000, - completion_tokens: 5000, - total_tokens: 25000, - cost: 20.0, - }, - ], - by_provider: vec![ - ProviderStats { - provider: "openai".to_string(), - requests: 100, - total_tokens: 50000, - cost: 25.0, - }, - ], - })) -} - -#[gotcha::api] -async fn get_user_by_id( - Path(id): Path, -) -> String { - format!("User ID: {}", id) -} - -// Test with tuple syntax (this should already work) -#[gotcha::api] -async fn get_user_tuple( - Path((id,)): Path<(Uuid,)>, -) -> String { - format!("User ID from tuple: {}", id) -} - -impl GotchaApp for App { - type State = AppState; - type Config = AppConfig; - - async fn state(&self, _config: &ConfigWrapper) -> Result> { - Ok(AppState {}) - } - - fn routes(&self, router: GotchaRouter>) -> GotchaRouter> { - router - .get("/admin/users/:user_id/usage", get_usage_stats) - .get("/users/:id", get_user_by_id) - .get("/users-tuple/:id", get_user_tuple) - } -} - -#[tokio::main] -async fn main() -> Result<(), Box> { - let app = App; - app.run().await?; - Ok(()) -} \ No newline at end of file