summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDawid Rycerz <dawid@rycerz.xyz>2025-07-16 23:37:24 +0300
committerDawid Rycerz <dawid@rycerz.xyz>2025-07-16 23:37:24 +0300
commitfa00c5863394c91a7b34680849908b1059e368f2 (patch)
treee357a7f322c644ce9487c4658e4a07ddb0e44611 /src
parentaf4680ee0577b28d0563ddc3d2677e8c96f4f5eb (diff)
feat: add openapi docs generation
Diffstat (limited to 'src')
-rw-r--r--src/health.rs13
-rw-r--r--src/locations.rs3
-rw-r--r--src/main.rs358
-rw-r--r--src/notifications.rs5
-rw-r--r--src/users.rs5
-rw-r--r--src/weather_thresholds.rs3
6 files changed, 371 insertions, 16 deletions
diff --git a/src/health.rs b/src/health.rs
index ddf949a..c3ae80a 100644
--- a/src/health.rs
+++ b/src/health.rs
@@ -1,6 +1,19 @@
use axum::{Json, response::IntoResponse};
use serde_json::json;
+#[utoipa::path(
+ get,
+ path = "/health",
+ responses(
+ (status = 200, description = "Health check", body = inline(HealthResponse))
+ ),
+ tag = "health"
+)]
pub async fn health_handler() -> impl IntoResponse {
Json(json!({"status": "ok"}))
}
+
+#[derive(utoipa::ToSchema, serde::Serialize)]
+pub struct HealthResponse {
+ pub status: String,
+}
diff --git a/src/locations.rs b/src/locations.rs
index 5430753..7f54bb6 100644
--- a/src/locations.rs
+++ b/src/locations.rs
@@ -1,7 +1,8 @@
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
+use utoipa::ToSchema;
-#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq)]
+#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq, ToSchema)]
pub struct Location {
pub id: i64,
pub latitude: f64,
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<UserRole>,
}
- #[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<Arc<SqlitePool>>,
@@ -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<i64>,
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<Arc<SqlitePool>>,
@@ -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<i64>,
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<i64>,
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<Arc<SqlitePool>>,
@@ -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<i64>,
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<Arc<SqlitePool>>,
@@ -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<i64>,
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<i64>,
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<String>,
}
- #[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<String>,
}
+ #[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<Arc<SqlitePool>>,
@@ -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<i64>,
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<Arc<SqlitePool>>,
@@ -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<i64>,
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<i64>,
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<String>,
pub message_template: Option<String>,
}
- #[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<String>,
pub message_template: Option<String>,
}
+ #[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<Arc<SqlitePool>>,
@@ -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<Arc<SqlitePool>>,
@@ -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<i64>,
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<i64>,
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<String>,
pub body_template: Option<String>,
}
- #[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<String>,
pub body_template: Option<String>,
}
+ #[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<Arc<SqlitePool>>,
@@ -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<Arc<SqlitePool>>,
@@ -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<i64>,
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<i64>,
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<SqlitePool>) -> Router {
Router::new()
.route("/health", get(health::health_handler))
+ .merge(SwaggerUi::new("/swagger-ui").url("/openapi.json", ApiDoc::openapi()))
.nest(
"/api/users",
Router::new()
diff --git a/src/notifications.rs b/src/notifications.rs
index 2dad552..9891b5e 100644
--- a/src/notifications.rs
+++ b/src/notifications.rs
@@ -1,7 +1,8 @@
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
+use utoipa::ToSchema;
-#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq)]
+#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq, ToSchema)]
pub struct NtfySettings {
pub id: i64,
pub user_id: i64,
@@ -23,7 +24,7 @@ pub struct NtfySettingsInput {
pub message_template: Option<String>,
}
-#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq)]
+#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq, ToSchema)]
pub struct SmtpSettings {
pub id: i64,
pub user_id: i64,
diff --git a/src/users.rs b/src/users.rs
index d3c1216..43f190d 100644
--- a/src/users.rs
+++ b/src/users.rs
@@ -1,15 +1,16 @@
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
+use utoipa::ToSchema;
use uuid::Uuid;
-#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq, Eq)]
+#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq, Eq, ToSchema)]
pub struct User {
pub id: i64,
pub user_id: String, // API token
pub role: UserRole,
}
-#[derive(Debug, Serialize, Deserialize, sqlx::Type, Clone, PartialEq, Eq, Default)]
+#[derive(Debug, Serialize, Deserialize, sqlx::Type, Clone, PartialEq, Eq, Default, ToSchema)]
#[sqlx(type_name = "TEXT")]
pub enum UserRole {
#[serde(rename = "user")]
diff --git a/src/weather_thresholds.rs b/src/weather_thresholds.rs
index ed95f13..a105bbc 100644
--- a/src/weather_thresholds.rs
+++ b/src/weather_thresholds.rs
@@ -1,7 +1,8 @@
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
+use utoipa::ToSchema;
-#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq)]
+#[derive(Debug, Serialize, Deserialize, FromRow, Clone, PartialEq, ToSchema)]
pub struct WeatherThreshold {
pub id: i64,
pub user_id: i64,