From 655507fd5a016cdd3bce44d2df47398ab840ad69 Mon Sep 17 00:00:00 2001 From: Kilerd Chan Date: Thu, 5 Feb 2026 01:51:26 +0800 Subject: [PATCH] feat: add OpenAPI schema generation support for Extension parameters - Add Extension to imports in openapi/schematic.rs - Implement ParameterProvider trait for Extension with empty parameter generation - Re-export Extension from lib.rs and prelude for user convenience - Add test to verify Extension parameters work with the api macro - Extension parameters are now properly excluded from OpenAPI specs, similar to State and Request This allows handlers to use Extension for middleware-injected values without polluting the OpenAPI documentation with parameters that don't come from the HTTP request. --- examples/extension/Cargo.toml | 12 ++ .../extension/configurations/application.toml | 6 + examples/extension/src/main.rs | 123 ++++++++++++++++++ gotcha/src/lib.rs | 2 +- gotcha/src/openapi/schematic.rs | 6 +- gotcha/src/prelude.rs | 2 +- .../tests/pass/openapi/extension_parameter.rs | 49 +++++++ gotcha_macro/src/schematic/named_struct.rs | 1 - 8 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 examples/extension/Cargo.toml create mode 100644 examples/extension/configurations/application.toml create mode 100644 examples/extension/src/main.rs create mode 100644 gotcha/tests/pass/openapi/extension_parameter.rs diff --git a/examples/extension/Cargo.toml b/examples/extension/Cargo.toml new file mode 100644 index 0000000..b64dcc7 --- /dev/null +++ b/examples/extension/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "extension" +version = "0.1.0" +edition = "2021" + +[dependencies] +gotcha = { path = "../../gotcha", features = ["openapi"] } +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +uuid = { version = "1", features = ["v4"] } +axum = "0.7" \ No newline at end of file diff --git a/examples/extension/configurations/application.toml b/examples/extension/configurations/application.toml new file mode 100644 index 0000000..1082b07 --- /dev/null +++ b/examples/extension/configurations/application.toml @@ -0,0 +1,6 @@ +[basic] +host = "127.0.0.1" +port = 3000 + +[application] +app_name = "Extension OpenAPI Example" \ No newline at end of file diff --git a/examples/extension/src/main.rs b/examples/extension/src/main.rs new file mode 100644 index 0000000..91a79b7 --- /dev/null +++ b/examples/extension/src/main.rs @@ -0,0 +1,123 @@ +//! Example demonstrating Extension usage with OpenAPI generation + +use gotcha::{ + api, async_trait, ConfigWrapper, Extension, GotchaApp, GotchaContext, GotchaRouter, Json, + Responder, Schematic, State +}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone)] +pub struct AuthContext { + pub user_id: String, + pub role: String, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +pub struct Config { + pub app_name: String, +} + +#[derive(Debug, Serialize, Deserialize, Schematic)] +pub struct UserResponse { + pub id: String, + pub name: String, + pub role: String, +} + +/// Get current user information +#[api(id = "get_current_user", group = "users")] +pub async fn get_current_user( + Extension(auth): Extension, + State(_config): State>, +) -> Json { + Json(UserResponse { + id: auth.user_id.clone(), + name: format!("User {}", auth.user_id), + role: auth.role, + }) +} + +#[derive(Debug, Serialize, Deserialize, Schematic)] +pub struct CreatePostRequest { + pub title: String, + pub content: String, +} + +#[derive(Debug, Serialize, Deserialize, Schematic)] +pub struct PostResponse { + pub id: String, + pub title: String, + pub content: String, + pub author_id: String, +} + +/// Create a new post +#[api(id = "create_post", group = "posts")] +pub async fn create_post( + Extension(auth): Extension, + Json(request): Json, +) -> Json { + Json(PostResponse { + id: uuid::Uuid::new_v4().to_string(), + title: request.title, + content: request.content, + author_id: auth.user_id, + }) +} + +/// Health check endpoint without auth +#[api(id = "health", group = "system")] +pub async fn health() -> Json { + Json(serde_json::json!({ "status": "healthy" })) +} + +pub struct App {} + +#[async_trait] +impl GotchaApp for App { + type State = (); + type Config = Config; + + fn routes(&self, router: GotchaRouter>) -> GotchaRouter> { + router + .get("/health", health) + .get("/user/me", get_current_user) + .post("/posts", create_post) + // Add middleware to inject the AuthContext + .layer(axum::middleware::from_fn(inject_auth_context)) + } + + fn state(&self, _config: &ConfigWrapper) -> impl std::future::Future>> + Send { + async { Ok(()) } + } +} + +// Middleware to inject AuthContext into requests +async fn inject_auth_context( + mut req: axum::extract::Request, + next: axum::middleware::Next, +) -> impl Responder { + // In a real application, you would extract this from a JWT token or session + let auth_context = AuthContext { + user_id: "user123".to_string(), + role: "admin".to_string(), + }; + + req.extensions_mut().insert(auth_context); + next.run(req).await +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("Starting Extension OpenAPI example server..."); + println!("Visit http://localhost:3000/scalar for API documentation"); + println!("Visit http://localhost:3000/openapi.json for OpenAPI spec"); + println!(); + println!("Available endpoints:"); + println!(" GET /health - Health check (no auth)"); + println!(" GET /user/me - Get current user (uses Extension)"); + println!(" POST /posts - Create post (uses Extension)"); + + App {}.run().await?; + Ok(()) +} \ No newline at end of file diff --git a/gotcha/src/lib.rs b/gotcha/src/lib.rs index 6c53e91..413627f 100644 --- a/gotcha/src/lib.rs +++ b/gotcha/src/lib.rs @@ -81,7 +81,7 @@ pub use async_trait::async_trait; use axum::extract::FromRef; -pub use axum::extract::{Json, Path, Query, State}; +pub use axum::extract::{Extension, Json, Path, Query, State}; pub use axum::response::IntoResponse as Responder; pub use axum::routing::{delete, get, patch, post, put}; pub use axum_macros::debug_handler; diff --git a/gotcha/src/openapi/schematic.rs b/gotcha/src/openapi/schematic.rs index f101fe4..9eba705 100644 --- a/gotcha/src/openapi/schematic.rs +++ b/gotcha/src/openapi/schematic.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeMap, HashMap, HashSet}; -use axum::extract::{Json, Path, Query, Request, State}; +use axum::extract::{Extension, Json, Path, Query, Request, State}; use bigdecimal::BigDecimal; use chrono::{DateTime, Utc}; use either::Either; @@ -52,7 +52,7 @@ pub trait Schematic { /// ParameterProvider is a trait that defines the value which can be used as a parameter. pub trait ParameterProvider { - fn generate(url: String) -> Either, RequestBody> { + fn generate(_url: String) -> Either, RequestBody> { Either::Left(vec![]) } } @@ -486,6 +486,8 @@ impl ParameterProvider for Query { impl ParameterProvider for State {} +impl ParameterProvider for Extension {} + impl ParameterProvider for Request {} impl ParameterProvider for axum::extract::multipart::Multipart { diff --git a/gotcha/src/prelude.rs b/gotcha/src/prelude.rs index 5bf2120..94cffe8 100644 --- a/gotcha/src/prelude.rs +++ b/gotcha/src/prelude.rs @@ -30,7 +30,7 @@ pub use crate::config::{ConfigWrapper, GotchaConfigLoader}; pub use crate::router::Responder; // Common Axum extractors and utilities -pub use axum::extract::{Json, Path, Query, State}; +pub use axum::extract::{Extension, Json, Path, Query, State}; pub use axum::http::{StatusCode, HeaderMap, Method}; pub use axum::response::{Html, Redirect, Response}; pub use axum::routing::{get, post, put, delete, patch}; diff --git a/gotcha/tests/pass/openapi/extension_parameter.rs b/gotcha/tests/pass/openapi/extension_parameter.rs new file mode 100644 index 0000000..0c7fe85 --- /dev/null +++ b/gotcha/tests/pass/openapi/extension_parameter.rs @@ -0,0 +1,49 @@ +//! Test that Extension parameters are properly handled in OpenAPI generation + +use gotcha::{api, Extension, Json, Schematic}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone)] +struct AuthContext { + user_id: String, +} + +#[derive(Serialize, Deserialize, Schematic)] +struct Request { + message: String, +} + +#[derive(Serialize, Deserialize, Schematic)] +struct Response { + message: String, +} + +/// Test endpoint with Extension parameter +#[api(id = "test_extension", group = "test")] +async fn handler_with_extension( + Extension(_auth): Extension, + Json(body): Json, +) -> Json { + Json(Response { + message: body.message, + }) +} + +/// Test endpoint with multiple Extension parameters +#[api(id = "test_multiple_extensions", group = "test")] +async fn handler_with_multiple_extensions( + Extension(_auth): Extension, + Extension(_config): Extension, + Json(body): Json, +) -> Json { + Json(Response { + message: body.message, + }) +} + +fn main() { + // This test verifies that Extension parameters compile correctly with the #[api] macro + // The fact that this compiles is the test - Extension implements ParameterProvider + // with an empty implementation, so it doesn't generate any OpenAPI parameters + println!("Extension parameters compile successfully with #[api] macro"); +} \ No newline at end of file diff --git a/gotcha_macro/src/schematic/named_struct.rs b/gotcha_macro/src/schematic/named_struct.rs index 2f70f54..7d9e285 100644 --- a/gotcha_macro/src/schematic/named_struct.rs +++ b/gotcha_macro/src/schematic/named_struct.rs @@ -1,6 +1,5 @@ use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::quote; -use syn::GenericParam; use crate::schematic::ParameterStructFieldOpt; use crate::utils::AttributesExt;