From 1866eef770de9350bbdf1f075a51ba2eff88af76 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 14 May 2026 11:41:12 +0200 Subject: [PATCH] feat(presentation): OpenAPI docs at /docs (Swagger) and /scalar --- crates/presentation/src/lib.rs | 1 + crates/presentation/src/openapi/api_keys.rs | 13 ++++ crates/presentation/src/openapi/auth.rs | 9 +++ crates/presentation/src/openapi/feed.rs | 13 ++++ crates/presentation/src/openapi/health.rs | 5 ++ crates/presentation/src/openapi/mod.rs | 61 +++++++++++++++++++ .../presentation/src/openapi/notifications.rs | 9 +++ crates/presentation/src/openapi/social.rs | 20 ++++++ crates/presentation/src/openapi/thoughts.rs | 15 +++++ crates/presentation/src/openapi/users.rs | 13 ++++ crates/presentation/src/routes.rs | 8 ++- 11 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 crates/presentation/src/openapi/api_keys.rs create mode 100644 crates/presentation/src/openapi/auth.rs create mode 100644 crates/presentation/src/openapi/feed.rs create mode 100644 crates/presentation/src/openapi/health.rs create mode 100644 crates/presentation/src/openapi/mod.rs create mode 100644 crates/presentation/src/openapi/notifications.rs create mode 100644 crates/presentation/src/openapi/social.rs create mode 100644 crates/presentation/src/openapi/thoughts.rs create mode 100644 crates/presentation/src/openapi/users.rs diff --git a/crates/presentation/src/lib.rs b/crates/presentation/src/lib.rs index 2b0c45a..74dc9a0 100644 --- a/crates/presentation/src/lib.rs +++ b/crates/presentation/src/lib.rs @@ -1,4 +1,5 @@ pub mod errors; +pub mod openapi; pub mod extractors; pub mod handlers; pub mod routes; diff --git a/crates/presentation/src/openapi/api_keys.rs b/crates/presentation/src/openapi/api_keys.rs new file mode 100644 index 0000000..bf75092 --- /dev/null +++ b/crates/presentation/src/openapi/api_keys.rs @@ -0,0 +1,13 @@ +use utoipa::OpenApi; +use api_types::{requests::CreateApiKeyRequest, responses::{ApiKeyResponse, CreatedApiKeyResponse}}; + +#[derive(OpenApi)] +#[openapi( + paths( + crate::handlers::api_keys::get_api_keys, + crate::handlers::api_keys::post_api_key, + crate::handlers::api_keys::delete_api_key_handler, + ), + components(schemas(CreateApiKeyRequest, ApiKeyResponse, CreatedApiKeyResponse)) +)] +pub struct ApiKeysDoc; diff --git a/crates/presentation/src/openapi/auth.rs b/crates/presentation/src/openapi/auth.rs new file mode 100644 index 0000000..7aaa3fc --- /dev/null +++ b/crates/presentation/src/openapi/auth.rs @@ -0,0 +1,9 @@ +use utoipa::OpenApi; +use api_types::{requests::{LoginRequest, RegisterRequest}, responses::{AuthResponse, ErrorResponse}}; + +#[derive(OpenApi)] +#[openapi( + paths(crate::handlers::auth::post_register, crate::handlers::auth::post_login), + components(schemas(RegisterRequest, LoginRequest, AuthResponse, ErrorResponse)) +)] +pub struct AuthDoc; diff --git a/crates/presentation/src/openapi/feed.rs b/crates/presentation/src/openapi/feed.rs new file mode 100644 index 0000000..90685d4 --- /dev/null +++ b/crates/presentation/src/openapi/feed.rs @@ -0,0 +1,13 @@ +use utoipa::OpenApi; + +#[derive(OpenApi)] +#[openapi( + paths( + crate::handlers::feed::home_feed, + crate::handlers::feed::public_feed, + crate::handlers::feed::search_handler, + crate::handlers::feed::user_thoughts_handler, + crate::handlers::feed::tag_thoughts_handler, + ), +)] +pub struct FeedDoc; diff --git a/crates/presentation/src/openapi/health.rs b/crates/presentation/src/openapi/health.rs new file mode 100644 index 0000000..cd5eb5b --- /dev/null +++ b/crates/presentation/src/openapi/health.rs @@ -0,0 +1,5 @@ +use utoipa::OpenApi; + +#[derive(OpenApi)] +#[openapi(paths(crate::handlers::health::health_handler))] +pub struct HealthDoc; diff --git a/crates/presentation/src/openapi/mod.rs b/crates/presentation/src/openapi/mod.rs new file mode 100644 index 0000000..1819b29 --- /dev/null +++ b/crates/presentation/src/openapi/mod.rs @@ -0,0 +1,61 @@ +mod api_keys; +mod auth; +mod feed; +mod health; +mod notifications; +mod social; +mod thoughts; +mod users; + +use axum::Router; +use utoipa::{ + Modify, OpenApi, + openapi::security::{ApiKey, ApiKeyValue, Http, HttpAuthScheme, SecurityScheme}, +}; +use utoipa_scalar::{Scalar, Servable}; +use utoipa_swagger_ui::SwaggerUi; + +struct SecurityAddon; + +impl Modify for SecurityAddon { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + let components = openapi.components.get_or_insert_with(Default::default); + components.add_security_scheme( + "bearer_auth", + SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)), + ); + components.add_security_scheme( + "api_key", + SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("X-Api-Key"))), + ); + } +} + +fn build() -> utoipa::openapi::OpenApi { + let mut api = auth::AuthDoc::openapi(); + api.info = utoipa::openapi::InfoBuilder::new() + .title("Thoughts API") + .version("2.0.0") + .description(Some( + "Federated social network API. Authenticate via `POST /auth/login` to get a Bearer token, \ + or use `X-Api-Key` header with a key from `POST /api-keys`." + )) + .build(); + api.merge(users::UsersDoc::openapi()); + api.merge(thoughts::ThoughtsDoc::openapi()); + api.merge(feed::FeedDoc::openapi()); + api.merge(social::SocialDoc::openapi()); + api.merge(notifications::NotificationsDoc::openapi()); + api.merge(api_keys::ApiKeysDoc::openapi()); + api.merge(health::HealthDoc::openapi()); + SecurityAddon.modify(&mut api); + api +} + +pub fn serve(router: Router) -> Router { + tracing::info!("API docs at /docs (Swagger UI) and /scalar (Scalar)"); + let spec = build(); + router + .merge(SwaggerUi::new("/docs").url("/openapi.json", spec.clone())) + .merge(Scalar::with_url("/scalar", spec)) +} diff --git a/crates/presentation/src/openapi/notifications.rs b/crates/presentation/src/openapi/notifications.rs new file mode 100644 index 0000000..dfd757f --- /dev/null +++ b/crates/presentation/src/openapi/notifications.rs @@ -0,0 +1,9 @@ +use utoipa::OpenApi; + +#[derive(OpenApi)] +#[openapi(paths( + crate::handlers::notifications::list_notifications, + crate::handlers::notifications::mark_notification_read, + crate::handlers::notifications::mark_all_read, +))] +pub struct NotificationsDoc; diff --git a/crates/presentation/src/openapi/social.rs b/crates/presentation/src/openapi/social.rs new file mode 100644 index 0000000..94ceda5 --- /dev/null +++ b/crates/presentation/src/openapi/social.rs @@ -0,0 +1,20 @@ +use utoipa::OpenApi; +use api_types::requests::SetTopFriendsRequest; + +#[derive(OpenApi)] +#[openapi( + paths( + crate::handlers::social::post_like, + crate::handlers::social::delete_like, + crate::handlers::social::post_boost, + crate::handlers::social::delete_boost, + crate::handlers::social::post_follow, + crate::handlers::social::delete_follow, + crate::handlers::social::post_block, + crate::handlers::social::delete_block, + crate::handlers::social::put_top_friends, + crate::handlers::social::get_top_friends_handler, + ), + components(schemas(SetTopFriendsRequest)) +)] +pub struct SocialDoc; diff --git a/crates/presentation/src/openapi/thoughts.rs b/crates/presentation/src/openapi/thoughts.rs new file mode 100644 index 0000000..a355ab0 --- /dev/null +++ b/crates/presentation/src/openapi/thoughts.rs @@ -0,0 +1,15 @@ +use utoipa::OpenApi; +use api_types::{requests::{CreateThoughtRequest, EditThoughtRequest}, responses::ErrorResponse}; + +#[derive(OpenApi)] +#[openapi( + paths( + crate::handlers::thoughts::post_thought, + crate::handlers::thoughts::get_thought_handler, + crate::handlers::thoughts::patch_thought, + crate::handlers::thoughts::delete_thought_handler, + crate::handlers::thoughts::get_thread_handler, + ), + components(schemas(CreateThoughtRequest, EditThoughtRequest, ErrorResponse)) +)] +pub struct ThoughtsDoc; diff --git a/crates/presentation/src/openapi/users.rs b/crates/presentation/src/openapi/users.rs new file mode 100644 index 0000000..f897238 --- /dev/null +++ b/crates/presentation/src/openapi/users.rs @@ -0,0 +1,13 @@ +use utoipa::OpenApi; +use api_types::{requests::UpdateProfileRequest, responses::{UserResponse, ErrorResponse}}; + +#[derive(OpenApi)] +#[openapi( + paths( + crate::handlers::users::get_me, + crate::handlers::users::get_user, + crate::handlers::users::patch_profile, + ), + components(schemas(UserResponse, UpdateProfileRequest, ErrorResponse)) +)] +pub struct UsersDoc; diff --git a/crates/presentation/src/routes.rs b/crates/presentation/src/routes.rs index 04511b8..2e10c70 100644 --- a/crates/presentation/src/routes.rs +++ b/crates/presentation/src/routes.rs @@ -12,7 +12,7 @@ use activitypub_base::{ ApFederationConfig, }; use activitypub_federation::config::FederationMiddleware; -use crate::{handlers::*, state::AppState}; +use crate::{handlers::*, openapi, state::AppState}; pub fn router(fed_config: &ApFederationConfig) -> Router { let api_routes = Router::new() @@ -79,8 +79,10 @@ pub fn router(fed_config: &ApFederationConfig) -> Router { .route("/users/{username}/followers", get(followers_handler)) .route("/users/{username}/following", get(following_handler)); - Router::new() + let combined = Router::new() .merge(api_routes) .merge(ap_routes) - .layer(FederationMiddleware::new(fed_config.0.clone())) + .layer(FederationMiddleware::new(fed_config.0.clone())); + + openapi::serve(combined) }