Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions examples/extension/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
6 changes: 6 additions & 0 deletions examples/extension/configurations/application.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[basic]
host = "127.0.0.1"
port = 3000

[application]
app_name = "Extension OpenAPI Example"
123 changes: 123 additions & 0 deletions examples/extension/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//! Example demonstrating Extension<T> 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<AuthContext>,
State(_config): State<ConfigWrapper<Config>>,
) -> Json<UserResponse> {
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<AuthContext>,
Json(request): Json<CreatePostRequest>,
) -> Json<PostResponse> {
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<serde_json::Value> {
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<GotchaContext<Self::State, Self::Config>>) -> GotchaRouter<GotchaContext<Self::State, Self::Config>> {
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<Self::Config>) -> impl std::future::Future<Output = Result<Self::State, Box<dyn std::error::Error>>> + 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<dyn std::error::Error>> {
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(())
}
2 changes: 1 addition & 1 deletion gotcha/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions gotcha/src/openapi/schematic.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<Vec<Parameter>, RequestBody> {
fn generate(_url: String) -> Either<Vec<Parameter>, RequestBody> {
Either::Left(vec![])
}
}
Expand Down Expand Up @@ -486,6 +486,8 @@ impl<T: Schematic> ParameterProvider for Query<T> {

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

impl<T> ParameterProvider for Extension<T> {}

impl ParameterProvider for Request {}

impl ParameterProvider for axum::extract::multipart::Multipart {
Expand Down
2 changes: 1 addition & 1 deletion gotcha/src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
49 changes: 49 additions & 0 deletions gotcha/tests/pass/openapi/extension_parameter.rs
Original file line number Diff line number Diff line change
@@ -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<AuthContext>,
Json(body): Json<Request>,
) -> Json<Response> {
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<AuthContext>,
Extension(_config): Extension<String>,
Json(body): Json<Request>,
) -> Json<Response> {
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<T> implements ParameterProvider
// with an empty implementation, so it doesn't generate any OpenAPI parameters
println!("Extension parameters compile successfully with #[api] macro");
}
1 change: 0 additions & 1 deletion gotcha_macro/src/schematic/named_struct.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Loading