Skip to content

Commit c2916fa

Browse files
authored
Merge pull request #20 from Kilerd/add-extension-schema-support
feat: add OpenAPI support for Extension<T> parameters
2 parents 73f9683 + 655507f commit c2916fa

8 files changed

Lines changed: 196 additions & 5 deletions

File tree

examples/extension/Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "extension"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
gotcha = { path = "../../gotcha", features = ["openapi"] }
8+
tokio = { version = "1", features = ["full"] }
9+
serde = { version = "1", features = ["derive"] }
10+
serde_json = "1"
11+
uuid = { version = "1", features = ["v4"] }
12+
axum = "0.7"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[basic]
2+
host = "127.0.0.1"
3+
port = 3000
4+
5+
[application]
6+
app_name = "Extension OpenAPI Example"

examples/extension/src/main.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
//! Example demonstrating Extension<T> usage with OpenAPI generation
2+
3+
use gotcha::{
4+
api, async_trait, ConfigWrapper, Extension, GotchaApp, GotchaContext, GotchaRouter, Json,
5+
Responder, Schematic, State
6+
};
7+
use serde::{Deserialize, Serialize};
8+
9+
#[derive(Clone)]
10+
pub struct AuthContext {
11+
pub user_id: String,
12+
pub role: String,
13+
}
14+
15+
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
16+
pub struct Config {
17+
pub app_name: String,
18+
}
19+
20+
#[derive(Debug, Serialize, Deserialize, Schematic)]
21+
pub struct UserResponse {
22+
pub id: String,
23+
pub name: String,
24+
pub role: String,
25+
}
26+
27+
/// Get current user information
28+
#[api(id = "get_current_user", group = "users")]
29+
pub async fn get_current_user(
30+
Extension(auth): Extension<AuthContext>,
31+
State(_config): State<ConfigWrapper<Config>>,
32+
) -> Json<UserResponse> {
33+
Json(UserResponse {
34+
id: auth.user_id.clone(),
35+
name: format!("User {}", auth.user_id),
36+
role: auth.role,
37+
})
38+
}
39+
40+
#[derive(Debug, Serialize, Deserialize, Schematic)]
41+
pub struct CreatePostRequest {
42+
pub title: String,
43+
pub content: String,
44+
}
45+
46+
#[derive(Debug, Serialize, Deserialize, Schematic)]
47+
pub struct PostResponse {
48+
pub id: String,
49+
pub title: String,
50+
pub content: String,
51+
pub author_id: String,
52+
}
53+
54+
/// Create a new post
55+
#[api(id = "create_post", group = "posts")]
56+
pub async fn create_post(
57+
Extension(auth): Extension<AuthContext>,
58+
Json(request): Json<CreatePostRequest>,
59+
) -> Json<PostResponse> {
60+
Json(PostResponse {
61+
id: uuid::Uuid::new_v4().to_string(),
62+
title: request.title,
63+
content: request.content,
64+
author_id: auth.user_id,
65+
})
66+
}
67+
68+
/// Health check endpoint without auth
69+
#[api(id = "health", group = "system")]
70+
pub async fn health() -> Json<serde_json::Value> {
71+
Json(serde_json::json!({ "status": "healthy" }))
72+
}
73+
74+
pub struct App {}
75+
76+
#[async_trait]
77+
impl GotchaApp for App {
78+
type State = ();
79+
type Config = Config;
80+
81+
fn routes(&self, router: GotchaRouter<GotchaContext<Self::State, Self::Config>>) -> GotchaRouter<GotchaContext<Self::State, Self::Config>> {
82+
router
83+
.get("/health", health)
84+
.get("/user/me", get_current_user)
85+
.post("/posts", create_post)
86+
// Add middleware to inject the AuthContext
87+
.layer(axum::middleware::from_fn(inject_auth_context))
88+
}
89+
90+
fn state(&self, _config: &ConfigWrapper<Self::Config>) -> impl std::future::Future<Output = Result<Self::State, Box<dyn std::error::Error>>> + Send {
91+
async { Ok(()) }
92+
}
93+
}
94+
95+
// Middleware to inject AuthContext into requests
96+
async fn inject_auth_context(
97+
mut req: axum::extract::Request,
98+
next: axum::middleware::Next,
99+
) -> impl Responder {
100+
// In a real application, you would extract this from a JWT token or session
101+
let auth_context = AuthContext {
102+
user_id: "user123".to_string(),
103+
role: "admin".to_string(),
104+
};
105+
106+
req.extensions_mut().insert(auth_context);
107+
next.run(req).await
108+
}
109+
110+
#[tokio::main]
111+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
112+
println!("Starting Extension OpenAPI example server...");
113+
println!("Visit http://localhost:3000/scalar for API documentation");
114+
println!("Visit http://localhost:3000/openapi.json for OpenAPI spec");
115+
println!();
116+
println!("Available endpoints:");
117+
println!(" GET /health - Health check (no auth)");
118+
println!(" GET /user/me - Get current user (uses Extension)");
119+
println!(" POST /posts - Create post (uses Extension)");
120+
121+
App {}.run().await?;
122+
Ok(())
123+
}

gotcha/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
8282
pub use async_trait::async_trait;
8383
use axum::extract::FromRef;
84-
pub use axum::extract::{Json, Path, Query, State};
84+
pub use axum::extract::{Extension, Json, Path, Query, State};
8585
pub use axum::response::IntoResponse as Responder;
8686
pub use axum::routing::{delete, get, patch, post, put};
8787
pub use axum_macros::debug_handler;

gotcha/src/openapi/schematic.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::collections::{BTreeMap, HashMap, HashSet};
22

3-
use axum::extract::{Json, Path, Query, Request, State};
3+
use axum::extract::{Extension, Json, Path, Query, Request, State};
44
use bigdecimal::BigDecimal;
55
use chrono::{DateTime, Utc};
66
use either::Either;
@@ -52,7 +52,7 @@ pub trait Schematic {
5252

5353
/// ParameterProvider is a trait that defines the value which can be used as a parameter.
5454
pub trait ParameterProvider {
55-
fn generate(url: String) -> Either<Vec<Parameter>, RequestBody> {
55+
fn generate(_url: String) -> Either<Vec<Parameter>, RequestBody> {
5656
Either::Left(vec![])
5757
}
5858
}
@@ -486,6 +486,8 @@ impl<T: Schematic> ParameterProvider for Query<T> {
486486

487487
impl<T> ParameterProvider for State<T> {}
488488

489+
impl<T> ParameterProvider for Extension<T> {}
490+
489491
impl ParameterProvider for Request {}
490492

491493
impl ParameterProvider for axum::extract::multipart::Multipart {

gotcha/src/prelude.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub use crate::config::{ConfigWrapper, GotchaConfigLoader};
3030
pub use crate::router::Responder;
3131

3232
// Common Axum extractors and utilities
33-
pub use axum::extract::{Json, Path, Query, State};
33+
pub use axum::extract::{Extension, Json, Path, Query, State};
3434
pub use axum::http::{StatusCode, HeaderMap, Method};
3535
pub use axum::response::{Html, Redirect, Response};
3636
pub use axum::routing::{get, post, put, delete, patch};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//! Test that Extension parameters are properly handled in OpenAPI generation
2+
3+
use gotcha::{api, Extension, Json, Schematic};
4+
use serde::{Deserialize, Serialize};
5+
6+
#[derive(Clone)]
7+
struct AuthContext {
8+
user_id: String,
9+
}
10+
11+
#[derive(Serialize, Deserialize, Schematic)]
12+
struct Request {
13+
message: String,
14+
}
15+
16+
#[derive(Serialize, Deserialize, Schematic)]
17+
struct Response {
18+
message: String,
19+
}
20+
21+
/// Test endpoint with Extension parameter
22+
#[api(id = "test_extension", group = "test")]
23+
async fn handler_with_extension(
24+
Extension(_auth): Extension<AuthContext>,
25+
Json(body): Json<Request>,
26+
) -> Json<Response> {
27+
Json(Response {
28+
message: body.message,
29+
})
30+
}
31+
32+
/// Test endpoint with multiple Extension parameters
33+
#[api(id = "test_multiple_extensions", group = "test")]
34+
async fn handler_with_multiple_extensions(
35+
Extension(_auth): Extension<AuthContext>,
36+
Extension(_config): Extension<String>,
37+
Json(body): Json<Request>,
38+
) -> Json<Response> {
39+
Json(Response {
40+
message: body.message,
41+
})
42+
}
43+
44+
fn main() {
45+
// This test verifies that Extension parameters compile correctly with the #[api] macro
46+
// The fact that this compiles is the test - Extension<T> implements ParameterProvider
47+
// with an empty implementation, so it doesn't generate any OpenAPI parameters
48+
println!("Extension parameters compile successfully with #[api] macro");
49+
}

gotcha_macro/src/schematic/named_struct.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use proc_macro2::{Span, TokenStream as TokenStream2};
22
use quote::quote;
3-
use syn::GenericParam;
43

54
use crate::schematic::ParameterStructFieldOpt;
65
use crate::utils::AttributesExt;

0 commit comments

Comments
 (0)