From fa00c5863394c91a7b34680849908b1059e368f2 Mon Sep 17 00:00:00 2001 From: Dawid Rycerz Date: Wed, 16 Jul 2025 23:37:24 +0300 Subject: feat: add openapi docs generation --- src/main.rs | 358 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 348 insertions(+), 10 deletions(-) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index 4cf72ff..924dd81 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,8 @@ use axum::routing::{post, put}; use axum::{Router, routing::get}; use serde_json::json; use sqlx::SqlitePool; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; mod auth; mod health; @@ -26,16 +28,25 @@ mod users_api { use serde::Deserialize; use std::sync::Arc; - #[derive(Deserialize)] + #[derive(Deserialize, utoipa::ToSchema)] pub struct CreateUser { pub role: Option, } - #[derive(Deserialize)] + #[derive(Deserialize, utoipa::ToSchema)] pub struct UpdateUser { pub role: UserRole, } + #[utoipa::path( + get, + path = "/api/users", + responses( + (status = 200, description = "List users", body = [User]), + (status = 401, description = "Unauthorized") + ), + tag = "users" + )] pub async fn list_users( AuthUser(_): AuthUser, State(pool): State>, @@ -44,6 +55,19 @@ mod users_api { repo.list_users().await.map(Json).map_err(|e| e.to_string()) } + #[utoipa::path( + get, + path = "/api/users/{id}", + params( + ("id" = i64, Path, description = "User ID") + ), + responses( + (status = 200, description = "Get user by ID", body = User), + (status = 404, description = "User not found"), + (status = 401, description = "Unauthorized") + ), + tag = "users" + )] pub async fn get_user( Path(id): Path, AuthUser(_): AuthUser, @@ -57,6 +81,16 @@ mod users_api { .ok_or_else(|| "User not found".to_string()) } + #[utoipa::path( + post, + path = "/api/users", + request_body = CreateUser, + responses( + (status = 200, description = "Create user", body = User), + (status = 401, description = "Unauthorized") + ), + tag = "users" + )] pub async fn create_user( AuthUser(_): AuthUser, State(pool): State>, @@ -69,6 +103,20 @@ mod users_api { .map_err(|e| e.to_string()) } + #[utoipa::path( + put, + path = "/api/users/{id}", + params( + ("id" = i64, Path, description = "User ID") + ), + request_body = UpdateUser, + responses( + (status = 200, description = "Update user", body = User), + (status = 404, description = "User not found"), + (status = 401, description = "Unauthorized") + ), + tag = "users" + )] pub async fn update_user( Path(id): Path, AuthUser(_): AuthUser, @@ -82,6 +130,19 @@ mod users_api { .map_err(|e| e.to_string()) } + #[utoipa::path( + delete, + path = "/api/users/{id}", + params( + ("id" = i64, Path, description = "User ID") + ), + responses( + (status = 200, description = "User deleted"), + (status = 404, description = "User not found"), + (status = 401, description = "Unauthorized") + ), + tag = "users" + )] pub async fn delete_user( Path(id): Path, AuthUser(_): AuthUser, @@ -103,18 +164,27 @@ mod locations_api { use serde::Deserialize; use std::sync::Arc; - #[derive(Deserialize)] + #[derive(Deserialize, utoipa::ToSchema)] pub struct CreateLocation { pub latitude: f64, pub longitude: f64, } - #[derive(Deserialize)] + #[derive(Deserialize, utoipa::ToSchema)] pub struct UpdateLocation { pub latitude: f64, pub longitude: f64, } + #[utoipa::path( + get, + path = "/api/locations", + responses( + (status = 200, description = "List locations", body = [Location]), + (status = 401, description = "Unauthorized") + ), + tag = "locations" + )] pub async fn list_locations( AuthUser(_): AuthUser, State(pool): State>, @@ -126,6 +196,19 @@ mod locations_api { .map_err(|e| e.to_string()) } + #[utoipa::path( + get, + path = "/api/locations/{id}", + params( + ("id" = i64, Path, description = "Location ID") + ), + responses( + (status = 200, description = "Get location by ID", body = Location), + (status = 404, description = "Location not found"), + (status = 401, description = "Unauthorized") + ), + tag = "locations" + )] pub async fn get_location( Path(id): Path, AuthUser(_): AuthUser, @@ -139,6 +222,16 @@ mod locations_api { .ok_or_else(|| "Location not found".to_string()) } + #[utoipa::path( + post, + path = "/api/locations", + request_body = CreateLocation, + responses( + (status = 200, description = "Create location", body = Location), + (status = 401, description = "Unauthorized") + ), + tag = "locations" + )] pub async fn create_location( AuthUser(user): AuthUser, State(pool): State>, @@ -151,6 +244,20 @@ mod locations_api { .map_err(|e| e.to_string()) } + #[utoipa::path( + put, + path = "/api/locations/{id}", + params( + ("id" = i64, Path, description = "Location ID") + ), + request_body = UpdateLocation, + responses( + (status = 200, description = "Update location", body = Location), + (status = 404, description = "Location not found"), + (status = 401, description = "Unauthorized") + ), + tag = "locations" + )] pub async fn update_location( Path(id): Path, AuthUser(_): AuthUser, @@ -164,6 +271,19 @@ mod locations_api { .map_err(|e| e.to_string()) } + #[utoipa::path( + delete, + path = "/api/locations/{id}", + params( + ("id" = i64, Path, description = "Location ID") + ), + responses( + (status = 200, description = "Location deleted"), + (status = 404, description = "Location not found"), + (status = 401, description = "Unauthorized") + ), + tag = "locations" + )] pub async fn delete_location( Path(id): Path, AuthUser(_): AuthUser, @@ -185,7 +305,7 @@ mod thresholds_api { use serde::Deserialize; use std::sync::Arc; - #[derive(Deserialize)] + #[derive(Deserialize, utoipa::ToSchema)] pub struct CreateThreshold { pub condition_type: String, pub threshold_value: f64, @@ -194,7 +314,7 @@ mod thresholds_api { pub description: Option, } - #[derive(Deserialize)] + #[derive(Deserialize, utoipa::ToSchema)] pub struct UpdateThreshold { pub condition_type: String, pub threshold_value: f64, @@ -203,6 +323,15 @@ mod thresholds_api { pub description: Option, } + #[utoipa::path( + get, + path = "/api/weather-thresholds", + responses( + (status = 200, description = "List weather thresholds", body = [WeatherThreshold]), + (status = 401, description = "Unauthorized") + ), + tag = "weather-thresholds" + )] pub async fn list_thresholds( AuthUser(user): AuthUser, State(pool): State>, @@ -214,6 +343,19 @@ mod thresholds_api { } } + #[utoipa::path( + get, + path = "/api/weather-thresholds/{id}", + params( + ("id" = i64, Path, description = "Threshold ID") + ), + responses( + (status = 200, description = "Get weather threshold by ID", body = WeatherThreshold), + (status = 404, description = "Threshold not found"), + (status = 401, description = "Unauthorized") + ), + tag = "weather-thresholds" + )] pub async fn get_threshold( Path(id): Path, AuthUser(user): AuthUser, @@ -227,6 +369,16 @@ mod thresholds_api { .ok_or_else(|| "Threshold not found".to_string()) } + #[utoipa::path( + post, + path = "/api/weather-thresholds", + request_body = CreateThreshold, + responses( + (status = 200, description = "Create weather threshold", body = WeatherThreshold), + (status = 401, description = "Unauthorized") + ), + tag = "weather-thresholds" + )] pub async fn create_threshold( AuthUser(user): AuthUser, State(pool): State>, @@ -246,6 +398,20 @@ mod thresholds_api { .map_err(|e| e.to_string()) } + #[utoipa::path( + put, + path = "/api/weather-thresholds/{id}", + params( + ("id" = i64, Path, description = "Threshold ID") + ), + request_body = UpdateThreshold, + responses( + (status = 200, description = "Update weather threshold", body = WeatherThreshold), + (status = 404, description = "Threshold not found"), + (status = 401, description = "Unauthorized") + ), + tag = "weather-thresholds" + )] pub async fn update_threshold( Path(id): Path, AuthUser(user): AuthUser, @@ -267,6 +433,19 @@ mod thresholds_api { .map_err(|e| e.to_string()) } + #[utoipa::path( + delete, + path = "/api/weather-thresholds/{id}", + params( + ("id" = i64, Path, description = "Threshold ID") + ), + responses( + (status = 200, description = "Threshold deleted"), + (status = 404, description = "Threshold not found"), + (status = 401, description = "Unauthorized") + ), + tag = "weather-thresholds" + )] pub async fn delete_threshold( Path(id): Path, AuthUser(user): AuthUser, @@ -293,7 +472,7 @@ mod notifications_api { use std::sync::Arc; // NTFY - #[derive(Deserialize)] + #[derive(Deserialize, utoipa::ToSchema)] pub struct CreateNtfy { pub enabled: bool, pub topic: String, @@ -302,7 +481,7 @@ mod notifications_api { pub title_template: Option, pub message_template: Option, } - #[derive(Deserialize)] + #[derive(Deserialize, utoipa::ToSchema)] pub struct UpdateNtfy { pub enabled: bool, pub topic: String, @@ -311,6 +490,16 @@ mod notifications_api { pub title_template: Option, pub message_template: Option, } + #[utoipa::path( + get, + path = "/api/ntfy-settings/me", + responses( + (status = 200, description = "Get NTFY settings for current user", body = NtfySettings), + (status = 404, description = "NTFY settings not found"), + (status = 401, description = "Unauthorized") + ), + tag = "notifications" + )] pub async fn get_ntfy_settings( AuthUser(user): AuthUser, State(pool): State>, @@ -322,6 +511,17 @@ mod notifications_api { .map(Json) .ok_or_else(|| "NTFY settings not found".to_string()) } + + #[utoipa::path( + post, + path = "/api/ntfy-settings", + request_body = CreateNtfy, + responses( + (status = 200, description = "Create NTFY settings", body = NtfySettings), + (status = 401, description = "Unauthorized") + ), + tag = "notifications" + )] pub async fn create_ntfy_settings( AuthUser(user): AuthUser, State(pool): State>, @@ -341,6 +541,21 @@ mod notifications_api { .map(Json) .map_err(|e| e.to_string()) } + + #[utoipa::path( + put, + path = "/api/ntfy-settings/{id}", + params( + ("id" = i64, Path, description = "NTFY settings ID") + ), + request_body = UpdateNtfy, + responses( + (status = 200, description = "Update NTFY settings", body = NtfySettings), + (status = 404, description = "NTFY settings not found"), + (status = 401, description = "Unauthorized") + ), + tag = "notifications" + )] pub async fn update_ntfy_settings( Path(id): Path, AuthUser(user): AuthUser, @@ -364,6 +579,20 @@ mod notifications_api { .map(Json) .map_err(|e| e.to_string()) } + + #[utoipa::path( + delete, + path = "/api/ntfy-settings/{id}", + params( + ("id" = i64, Path, description = "NTFY settings ID") + ), + responses( + (status = 200, description = "NTFY settings deleted"), + (status = 404, description = "NTFY settings not found"), + (status = 401, description = "Unauthorized") + ), + tag = "notifications" + )] pub async fn delete_ntfy_settings( Path(id): Path, AuthUser(_): AuthUser, @@ -374,7 +603,7 @@ mod notifications_api { } // SMTP - #[derive(Deserialize)] + #[derive(Deserialize, utoipa::ToSchema)] pub struct CreateSmtp { pub enabled: bool, pub email: String, @@ -388,7 +617,7 @@ mod notifications_api { pub subject_template: Option, pub body_template: Option, } - #[derive(Deserialize)] + #[derive(Deserialize, utoipa::ToSchema)] pub struct UpdateSmtp { pub enabled: bool, pub email: String, @@ -402,6 +631,16 @@ mod notifications_api { pub subject_template: Option, pub body_template: Option, } + #[utoipa::path( + get, + path = "/api/smtp-settings/me", + responses( + (status = 200, description = "Get SMTP settings for current user", body = SmtpSettings), + (status = 404, description = "SMTP settings not found"), + (status = 401, description = "Unauthorized") + ), + tag = "notifications" + )] pub async fn get_smtp_settings( AuthUser(user): AuthUser, State(pool): State>, @@ -413,6 +652,17 @@ mod notifications_api { .map(Json) .ok_or_else(|| "SMTP settings not found".to_string()) } + + #[utoipa::path( + post, + path = "/api/smtp-settings", + request_body = CreateSmtp, + responses( + (status = 200, description = "Create SMTP settings", body = SmtpSettings), + (status = 401, description = "Unauthorized") + ), + tag = "notifications" + )] pub async fn create_smtp_settings( AuthUser(user): AuthUser, State(pool): State>, @@ -437,6 +687,21 @@ mod notifications_api { .map(Json) .map_err(|e| e.to_string()) } + + #[utoipa::path( + put, + path = "/api/smtp-settings/{id}", + params( + ("id" = i64, Path, description = "SMTP settings ID") + ), + request_body = UpdateSmtp, + responses( + (status = 200, description = "Update SMTP settings", body = SmtpSettings), + (status = 404, description = "SMTP settings not found"), + (status = 401, description = "Unauthorized") + ), + tag = "notifications" + )] pub async fn update_smtp_settings( Path(id): Path, AuthUser(user): AuthUser, @@ -465,6 +730,20 @@ mod notifications_api { .map(Json) .map_err(|e| e.to_string()) } + + #[utoipa::path( + delete, + path = "/api/smtp-settings/{id}", + params( + ("id" = i64, Path, description = "SMTP settings ID") + ), + responses( + (status = 200, description = "SMTP settings deleted"), + (status = 404, description = "SMTP settings not found"), + (status = 401, description = "Unauthorized") + ), + tag = "notifications" + )] pub async fn delete_smtp_settings( Path(id): Path, AuthUser(_): AuthUser, @@ -483,9 +762,68 @@ pub async fn not_found() -> axum::response::Response { error_response(StatusCode::NOT_FOUND, "Not Found") } +#[derive(OpenApi)] +#[openapi( + paths( + users_api::list_users, + users_api::get_user, + users_api::create_user, + users_api::update_user, + users_api::delete_user, + locations_api::list_locations, + locations_api::get_location, + locations_api::create_location, + locations_api::update_location, + locations_api::delete_location, + thresholds_api::list_thresholds, + thresholds_api::get_threshold, + thresholds_api::create_threshold, + thresholds_api::update_threshold, + thresholds_api::delete_threshold, + notifications_api::get_ntfy_settings, + notifications_api::create_ntfy_settings, + notifications_api::update_ntfy_settings, + notifications_api::delete_ntfy_settings, + notifications_api::get_smtp_settings, + notifications_api::create_smtp_settings, + notifications_api::update_smtp_settings, + notifications_api::delete_smtp_settings, + health::health_handler, + ), + components( + schemas( + users::User, + users::UserRole, + users_api::CreateUser, + users_api::UpdateUser, + locations::Location, + locations_api::CreateLocation, + locations_api::UpdateLocation, + weather_thresholds::WeatherThreshold, + thresholds_api::CreateThreshold, + thresholds_api::UpdateThreshold, + notifications::NtfySettings, + notifications::SmtpSettings, + notifications_api::CreateNtfy, + notifications_api::UpdateNtfy, + notifications_api::CreateSmtp, + notifications_api::UpdateSmtp, + ) + ), + tags( + (name = "users", description = "User management endpoints"), + (name = "locations", description = "Location management endpoints"), + (name = "weather-thresholds", description = "Weather threshold endpoints"), + (name = "notifications", description = "Notification settings endpoints"), + (name = "health", description = "Health check endpoint"), + ) +)] +pub struct ApiDoc; + pub fn app_with_state(pool: std::sync::Arc) -> Router { Router::new() .route("/health", get(health::health_handler)) + .merge(SwaggerUi::new("/swagger-ui").url("/openapi.json", ApiDoc::openapi())) .nest( "/api/users", Router::new() -- cgit v1.2.3