diff options
| author | Dawid Rycerz <dawid@rycerz.xyz> | 2025-07-16 23:37:24 +0300 |
|---|---|---|
| committer | Dawid Rycerz <dawid@rycerz.xyz> | 2025-07-16 23:37:24 +0300 |
| commit | fa00c5863394c91a7b34680849908b1059e368f2 (patch) | |
| tree | e357a7f322c644ce9487c4658e4a07ddb0e44611 | |
| parent | af4680ee0577b28d0563ddc3d2677e8c96f4f5eb (diff) | |
feat: add openapi docs generation
| -rw-r--r-- | Cargo.lock | 201 | ||||
| -rw-r--r-- | Cargo.toml | 3 | ||||
| -rw-r--r-- | src/health.rs | 13 | ||||
| -rw-r--r-- | src/locations.rs | 3 | ||||
| -rw-r--r-- | src/main.rs | 358 | ||||
| -rw-r--r-- | src/notifications.rs | 5 | ||||
| -rw-r--r-- | src/users.rs | 5 | ||||
| -rw-r--r-- | src/weather_thresholds.rs | 3 |
8 files changed, 575 insertions, 16 deletions
@@ -66,6 +66,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + +[[package]] name = "async-trait" version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -342,6 +351,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] name = "crossbeam-deque" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -397,6 +415,17 @@ dependencies = [ ] [[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "deunicode" version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -528,6 +557,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "libz-rs-sys", + "miniz_oxide", +] + +[[package]] name = "flume" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1134,6 +1174,7 @@ checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown 0.15.4", + "serde", ] [[package]] @@ -1237,6 +1278,15 @@ dependencies = [ ] [[package]] +name = "libz-rs-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +dependencies = [ + "zlib-rs", +] + +[[package]] name = "linux-raw-sys" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1293,6 +1343,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1499,6 +1559,12 @@ dependencies = [ ] [[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] name = "pem-rfc7468" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1849,6 +1915,40 @@ dependencies = [ ] [[package]] +name = "rust-embed" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] name = "rustc-demangle" version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2090,10 +2190,19 @@ dependencies = [ "tokio-task-scheduler", "tower", "tower-http", + "utoipa", + "utoipa-axum", + "utoipa-swagger-ui", "uuid", ] [[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] name = "siphasher" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2788,6 +2897,12 @@ dependencies = [ ] [[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] name = "unicode-bidi" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2838,6 +2953,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] +name = "utoipa" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-axum" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c25bae5bccc842449ec0c5ddc5cbb6a3a1eaeac4503895dc105a1138f8234a0" +dependencies = [ + "axum", + "paste", + "tower-layer", + "tower-service", + "utoipa", +] + +[[package]] +name = "utoipa-gen" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "9.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55" +dependencies = [ + "axum", + "base64", + "mime_guess", + "regex", + "rust-embed", + "serde", + "serde_json", + "url", + "utoipa", + "zip", +] + +[[package]] name = "uuid" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3438,3 +3607,35 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zip" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap", + "memchr", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] @@ -19,6 +19,9 @@ tokio-task-scheduler = "1.0.0" tower = "0.5.2" tower-http = "0.6.6" uuid = "1.17.0" +utoipa = "5.4.0" +utoipa-axum = "0.2.0" +utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } [dev-dependencies] anyhow = "1.0.98" 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, |
