diff --git a/api-backend/Cargo.lock b/api-backend/Cargo.lock index 488a1fb7c..b1121d786 100644 --- a/api-backend/Cargo.lock +++ b/api-backend/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -148,7 +148,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.1", "tokio", - "tower 0.5.1", + "tower", "tower-layer", "tower-service", "tracing", @@ -714,14 +714,17 @@ dependencies = [ "serde", "serde_json", "tokio", - "tower 0.4.13", + "tower", "tower-http", "tower-service", "tracing", + "utoipa", + "utoipa-axum", + "utoipa-scalar", ] [[package]] -name = "hartex_backend_layers" +name = "hartex_backend_extractors" version = "0.13.0" [[package]] @@ -731,6 +734,8 @@ dependencies = [ "axum", "hartex_discord_configuration_models", "serde", + "utoipa", + "utoipa-axum", ] [[package]] @@ -744,6 +749,7 @@ dependencies = [ "hartex_log", "serde_json", "time", + "utoipa", ] [[package]] @@ -983,6 +989,7 @@ checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown 0.14.5", + "serde", ] [[package]] @@ -1347,6 +1354,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2023,17 +2036,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower" version = "0.5.1" @@ -2186,6 +2188,54 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "utoipa" +version = "5.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d9ba0ade4e2f024cd1842dfbaf9dbc540639fc082299acf7649d71bd14eaca3" +dependencies = [ + "indexmap 2.5.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-axum" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1370cc4a8eee751c4d2a729566d83d1568212320a20581c7c72c2d76ab80ed37" +dependencies = [ + "axum", + "paste", + "tower-layer", + "tower-service", + "utoipa", +] + +[[package]] +name = "utoipa-gen" +version = "5.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf390d6503c9c9eac988447c38ba934a707b0b768b14511a493b4fc0e8ecb00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.82", +] + +[[package]] +name = "utoipa-scalar" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1291aa7a2223c2f8399d1c6627ca0ba57ca0d7ecac762a2094a9dfd6376445a" +dependencies = [ + "axum", + "serde", + "serde_json", + "utoipa", +] + [[package]] name = "version_check" version = "0.9.5" diff --git a/api-backend/Cargo.toml b/api-backend/Cargo.toml index dca2ebb8d..b2ed39034 100644 --- a/api-backend/Cargo.toml +++ b/api-backend/Cargo.toml @@ -1,7 +1,7 @@ [workspace] members = [ "hartex-backend-driver", - "hartex-backend-layers", + "hartex-backend-extractors", "hartex-backend-models", "hartex-backend-routes", ] diff --git a/api-backend/hartex-backend-driver/Cargo.toml b/api-backend/hartex-backend-driver/Cargo.toml index 6490d3a71..dabdfef8d 100644 --- a/api-backend/hartex-backend-driver/Cargo.toml +++ b/api-backend/hartex-backend-driver/Cargo.toml @@ -25,9 +25,12 @@ miette = { version = "7.2.0", features = ["fancy"] } serde = "1.0.210" serde_json = "1.0.128" tokio = { version = "1.40.0", features = ["full"] } -tower = "0.4.13" +tower = "0.5.1" tower-http = { version = "0.6.1", features = ["timeout", "trace"] } tower-service = "0.3.3" tracing = { version = "0.1.40", features = ["log-always"] } +utoipa = "5.1.3" +utoipa-axum = "0.1.2" +utoipa-scalar = { version = "0.2.0", features = ["axum"] } [features] diff --git a/api-backend/hartex-backend-driver/src/main.rs b/api-backend/hartex-backend-driver/src/main.rs index 4e2ddee57..1fb9d176e 100644 --- a/api-backend/hartex-backend-driver/src/main.rs +++ b/api-backend/hartex-backend-driver/src/main.rs @@ -36,8 +36,6 @@ use std::env; use std::future; use std::time::Duration; -use axum::routing::post; -use axum::Router; use bb8_postgres::bb8::Pool; use bb8_postgres::tokio_postgres::NoTls; use bb8_postgres::PostgresConnectionManager; @@ -49,6 +47,11 @@ use tokio::net::TcpListener; use tokio::signal; use tower_http::timeout::TimeoutLayer; use tower_http::trace::TraceLayer; +use utoipa::openapi::Info; +use utoipa_axum::router::OpenApiRouter; +use utoipa_axum::routes; +use utoipa_scalar::Scalar; +use utoipa_scalar::Servable; /// # Entry Point /// @@ -78,21 +81,23 @@ pub async fn main() -> miette::Result<()> { let pool = Pool::builder().build(manager).await.into_diagnostic()?; log::debug!("starting axum server"); - let app = Router::new() + let (app, mut openapi) = OpenApiRouter::new() .layer(TraceLayer::new_for_http()) .layer(TimeoutLayer::new(Duration::from_secs(30))) - .route( - "/api/:version/stats/uptime", - post(hartex_backend_routes::uptime::post_uptime) - .patch(hartex_backend_routes::uptime::patch_uptime), - ) - .with_state(pool); + .routes(routes!( + hartex_backend_routes::uptime::get_uptime + )) + .with_state(pool) + .split_for_parts(); let domain = env::var("API_DOMAIN").into_diagnostic()?; let listener = TcpListener::bind(&domain).await.into_diagnostic()?; - log::debug!("listening on {domain}"); + log::debug!("listening on {}", &domain); - axum::serve(listener, app) + openapi.info = Info::new("HarTex API", env!("CARGO_PKG_VERSION")); + let router = app.merge(Scalar::with_url("/openapi", openapi)); + + axum::serve(listener, router) .with_graceful_shutdown(shutdown()) .await .into_diagnostic()?; @@ -118,7 +123,7 @@ async fn shutdown() { .recv() .await; }; - + #[cfg(not(unix))] let terminate = future::pending::<()>(); diff --git a/api-backend/hartex-backend-layers/Cargo.toml b/api-backend/hartex-backend-extractors/Cargo.toml similarity index 82% rename from api-backend/hartex-backend-layers/Cargo.toml rename to api-backend/hartex-backend-extractors/Cargo.toml index 2f0edaf95..19c979c5f 100644 --- a/api-backend/hartex-backend-layers/Cargo.toml +++ b/api-backend/hartex-backend-extractors/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "hartex_backend_layers" +name = "hartex_backend_extractors" version = "0.13.0" edition = "2021" description = """ -Backend middleware +Backend extractors """ license = "AGPL-3.0-or-later" rust-version = "1.83.0" diff --git a/api-backend/hartex-backend-layers/src/lib.rs b/api-backend/hartex-backend-extractors/src/lib.rs similarity index 89% rename from api-backend/hartex-backend-layers/src/lib.rs rename to api-backend/hartex-backend-extractors/src/lib.rs index 3357b7cd5..0d9dfa73a 100644 --- a/api-backend/hartex-backend-layers/src/lib.rs +++ b/api-backend/hartex-backend-extractors/src/lib.rs @@ -20,9 +20,9 @@ * with HarTex. If not, see . */ -//! # Backend Layers +//! # Backend Extractors //! -//! This crate defines certain middleware layers for use with the Axum HTTP server. +//! This crate defines certain extractors for use with the Axum HTTP server. #![deny(clippy::pedantic)] #![deny(unsafe_code)] diff --git a/api-backend/hartex-backend-models/Cargo.toml b/api-backend/hartex-backend-models/Cargo.toml index a3137e21f..fef091d4c 100644 --- a/api-backend/hartex-backend-models/Cargo.toml +++ b/api-backend/hartex-backend-models/Cargo.toml @@ -15,5 +15,7 @@ hartex_discord_configuration_models = { path = "../../discord-frontend/hartex-di axum = "0.7.7" serde = { version = "1.0.210", features = ["derive"] } +utoipa = "5.1.3" +utoipa-axum = "0.1.2" [features] diff --git a/api-backend/hartex-backend-models/src/lib.rs b/api-backend/hartex-backend-models/src/lib.rs index dce29e49e..7f3c899df 100644 --- a/api-backend/hartex-backend-models/src/lib.rs +++ b/api-backend/hartex-backend-models/src/lib.rs @@ -28,52 +28,13 @@ #![deny(unsafe_code)] #![deny(warnings)] -use std::collections::HashMap; - -use axum::async_trait; -use axum::extract::FromRequestParts; -use axum::extract::Path; -use axum::http::request::Parts; -use axum::http::StatusCode; -use axum::response::IntoResponse; -use axum::response::Response as AxumResponse; use axum::Json; -use axum::RequestPartsExt; use serde::Deserialize; use serde::Serialize; pub use hartex_discord_configuration_models as config; pub mod uptime; -/// Specifies the API version to be used for a given API request. -#[derive(Copy, Clone, Debug)] -pub enum APIVersion { - /// Version 0.11.0 of the backend API. - V0_11_0, -} - -#[async_trait] -impl FromRequestParts for APIVersion -where - S: Send + Sync, -{ - type Rejection = AxumResponse; - - async fn from_request_parts(parts: &mut Parts, _: &S) -> Result { - let parameters: Path> = - parts.extract().await.map_err(IntoResponse::into_response)?; - - let version = parameters - .get("version") - .ok_or_else(|| (StatusCode::NOT_FOUND, "version not specified").into_response())?; - - match version.as_str() { - "v0110" | "v1" => Ok(APIVersion::V0_11_0), - _ => Err((StatusCode::NOT_FOUND, "unknown version specified").into_response()), - } - } -} - /// An API response object. /// /// This is the object returned by a certain API endpoint. diff --git a/api-backend/hartex-backend-models/src/uptime.rs b/api-backend/hartex-backend-models/src/uptime.rs index 369398104..ec5d39ddb 100644 --- a/api-backend/hartex-backend-models/src/uptime.rs +++ b/api-backend/hartex-backend-models/src/uptime.rs @@ -26,33 +26,35 @@ use serde::Deserialize; use serde::Serialize; +use utoipa::IntoParams; +use utoipa::ToSchema; /// An uptime query. #[allow(clippy::module_name_repetitions)] -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, IntoParams, Serialize)] pub struct UptimeQuery { - component_name: String, + component: String, } impl UptimeQuery { /// Create a new uptime query with the component name to search for. #[must_use] - pub fn new(component_name: &str) -> Self { + pub fn new(component: &str) -> Self { Self { - component_name: component_name.to_string(), + component: component.to_string(), } } /// The component name to search for in this uptime query. #[must_use] pub fn component_name(&self) -> &str { - self.component_name.as_str() + self.component.as_str() } } /// A response to an uptime query. #[allow(clippy::module_name_repetitions)] -#[derive(Clone, Deserialize, Serialize)] +#[derive(Clone, Deserialize, Serialize, ToSchema)] pub struct UptimeResponse { start_timestamp: u128, } diff --git a/api-backend/hartex-backend-routes/Cargo.toml b/api-backend/hartex-backend-routes/Cargo.toml index 33e9474ec..92301a57f 100644 --- a/api-backend/hartex-backend-routes/Cargo.toml +++ b/api-backend/hartex-backend-routes/Cargo.toml @@ -21,5 +21,6 @@ axum = { version = "0.7.7", features = ["json", "macros"] } bb8-postgres = "0.8.1" serde_json = "1.0.128" time = "0.3.36" +utoipa = "5.1.3" [features] diff --git a/api-backend/hartex-backend-routes/src/uptime.rs b/api-backend/hartex-backend-routes/src/uptime.rs index 9dde32945..aa76b63b2 100644 --- a/api-backend/hartex-backend-routes/src/uptime.rs +++ b/api-backend/hartex-backend-routes/src/uptime.rs @@ -24,6 +24,7 @@ /// /// Routes interacting with the uptime API. +use axum::extract::Query; use axum::extract::State; use axum::http::StatusCode; use axum::Json; @@ -34,25 +35,26 @@ use bb8_postgres::PostgresConnectionManager; use hartex_backend_models::uptime::UptimeQuery; use hartex_backend_models::uptime::UptimeResponse; use hartex_backend_models::uptime::UptimeUpdate; -use hartex_backend_models::APIVersion; use hartex_backend_models::Response; use hartex_database_queries::api_backend::queries::start_timestamp_select_by_component::select_start_timestamp_by_component; use hartex_database_queries::api_backend::queries::start_timestamp_upsert::start_timestamp_upsert; -use time::OffsetDateTime; use hartex_log::log; +use time::OffsetDateTime; -/// # `PATCH /stats/uptime` -/// -/// Update the uptime of a certain component. -#[allow(clippy::cast_possible_truncation)] +/// Get component uptime #[allow(clippy::cast_sign_loss)] #[allow(clippy::missing_panics_doc)] // this function cannot panic #[allow(clippy::module_name_repetitions)] -pub async fn patch_uptime( - _: APIVersion, +#[utoipa::path( + get, + path = "/api/v1/stats/uptime", + params(UptimeQuery), + responses((status = 200, description = "Uptime retrieved successfully", body = UptimeResponse)) +)] +pub async fn get_uptime( State(pool): State>>, - Json(query): Json, -) -> (StatusCode, Json>) { + Query(query): Query, +) -> (StatusCode, Json>) { log::trace!("retrieving connection from database pool"); let result = pool.get().await; if result.is_err() { @@ -65,43 +67,42 @@ pub async fn patch_uptime( let connection = result.unwrap(); let client = connection.client(); - log::trace!("updating timestamp"); - let Ok(timestamp) = OffsetDateTime::from_unix_timestamp(query.start_timestamp() as i64) else { - // FIXME: return a better status code as the timestamp is out of range if this branch is reached - // just 500 for now - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Response::internal_server_error(), - ); - }; - let result = start_timestamp_upsert() - .bind(client, &query.component_name(), ×tamp) + log::trace!("querying timestamp"); + let result = select_start_timestamp_by_component() + .bind(client, &query.component_name()) + .one() .await; + // FIXME: figure out whether the data is actually not found and return 404 if result.is_err() { + log::error!("{:?}", result.unwrap_err()); + return ( StatusCode::INTERNAL_SERVER_ERROR, Response::internal_server_error(), ); } + let data = result.unwrap(); ( StatusCode::OK, - Response::ok(()), + Response::ok(UptimeResponse::with_start_timestamp( + data.timestamp.unix_timestamp() as u128, + )), ) } -/// # `POST /stats/uptime` +/// # `PATCH /stats/uptime` /// -/// Obtain the uptime of a certain component. +/// Update the uptime of a certain component. +#[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss)] #[allow(clippy::missing_panics_doc)] // this function cannot panic #[allow(clippy::module_name_repetitions)] -pub async fn post_uptime( - _: APIVersion, +pub async fn patch_uptime( State(pool): State>>, - Json(query): Json, -) -> (StatusCode, Json>) { + Json(query): Json, +) -> (StatusCode, Json>) { log::trace!("retrieving connection from database pool"); let result = pool.get().await; if result.is_err() { @@ -114,27 +115,28 @@ pub async fn post_uptime( let connection = result.unwrap(); let client = connection.client(); - log::trace!("querying timestamp"); - let result = select_start_timestamp_by_component() - .bind(client, &query.component_name()) - .one() + log::trace!("updating timestamp"); + let Ok(timestamp) = OffsetDateTime::from_unix_timestamp(query.start_timestamp() as i64) else { + // FIXME: return a better status code as the timestamp is out of range if this branch is reached + // just 500 for now + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Response::internal_server_error(), + ); + }; + let result = start_timestamp_upsert() + .bind(client, &query.component_name(), ×tamp) .await; - // FIXME: figure out whether the data is actually not found and return 404 if result.is_err() { - log::error!("{:?}", result.unwrap_err()); - return ( StatusCode::INTERNAL_SERVER_ERROR, Response::internal_server_error(), ); } - let data = result.unwrap(); ( StatusCode::OK, - Response::ok(UptimeResponse::with_start_timestamp( - data.timestamp.unix_timestamp() as u128, - )), + Response::ok(()), ) } diff --git a/database/hartex-database-migrate/build.rs b/database/hartex-database-migrate/build.rs index 2c42f13da..5b9b08223 100644 --- a/database/hartex-database-migrate/build.rs +++ b/database/hartex-database-migrate/build.rs @@ -21,6 +21,6 @@ */ pub fn main() { - println!("cargo:rerun-if-changed=api-backend-migrations"); - println!("cargo:rerun-if-changed=discord-frontend-migrations"); + println!("cargo::rerun-if-changed=api-backend-migrations"); + println!("cargo::rerun-if-changed=discord-frontend-migrations"); } diff --git a/database/hartex-database-queries/build.rs b/database/hartex-database-queries/build.rs index 3cc13405f..51132131d 100644 --- a/database/hartex-database-queries/build.rs +++ b/database/hartex-database-queries/build.rs @@ -32,7 +32,7 @@ pub fn main() { } let api_backend_queries_path = "queries/api_backend"; - println!("cargo:rerun-if-changed={api_backend_queries_path}"); + println!("cargo::rerun-if-changed={api_backend_queries_path}"); let url = env::var("API_PGSQL_URL").unwrap(); cornucopia::generate_live( @@ -47,7 +47,7 @@ pub fn main() { .unwrap(); let configuration_queries_path = "queries/configuration"; - println!("cargo:rerun-if-changed={configuration_queries_path}"); + println!("cargo::rerun-if-changed={configuration_queries_path}"); let url = env::var("HARTEX_NIGHTLY_PGSQL_URL").unwrap(); cornucopia::generate_live( @@ -62,7 +62,7 @@ pub fn main() { .unwrap(); let discord_frontend_queries_path = "queries/discord_frontend"; - println!("cargo:rerun-if-changed={discord_frontend_queries_path}"); + println!("cargo::rerun-if-changed={discord_frontend_queries_path}"); let url = env::var("HARTEX_NIGHTLY_PGSQL_URL").unwrap(); cornucopia::generate_live( diff --git a/discord-frontend/Cargo.lock b/discord-frontend/Cargo.lock index 5b9c98b31..bf2dd12f4 100644 --- a/discord-frontend/Cargo.lock +++ b/discord-frontend/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -1162,6 +1162,8 @@ dependencies = [ "axum", "hartex_discord_configuration_models", "serde", + "utoipa", + "utoipa-axum", ] [[package]] @@ -1862,6 +1864,7 @@ checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown 0.14.5", + "serde", ] [[package]] @@ -1988,7 +1991,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -4058,6 +4061,42 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "utoipa" +version = "5.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d9ba0ade4e2f024cd1842dfbaf9dbc540639fc082299acf7649d71bd14eaca3" +dependencies = [ + "indexmap 2.5.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-axum" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1370cc4a8eee751c4d2a729566d83d1568212320a20581c7c72c2d76ab80ed37" +dependencies = [ + "axum", + "paste", + "tower-layer", + "tower-service", + "utoipa", +] + +[[package]] +name = "utoipa-gen" +version = "5.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf390d6503c9c9eac988447c38ba934a707b0b768b14511a493b4fc0e8ecb00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.86", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -4283,7 +4322,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/localization/hartex-localization-core/build.rs b/localization/hartex-localization-core/build.rs index 30ed7da29..36804fb77 100644 --- a/localization/hartex-localization-core/build.rs +++ b/localization/hartex-localization-core/build.rs @@ -21,5 +21,5 @@ */ pub fn main() { - println!("cargo:rerun-if-changed=../locales"); + println!("cargo::rerun-if-changed=../locales"); } diff --git a/tools/bootstrap/build.rs b/tools/bootstrap/build.rs index 9a3a26017..5f5acab9e 100644 --- a/tools/bootstrap/build.rs +++ b/tools/bootstrap/build.rs @@ -25,5 +25,5 @@ use std::env; pub fn main() { let target = env::var("TARGET").expect("cannot find build target"); - println!("cargo:rustc-env=BOOTSTRAP_TARGET={target}"); + println!("cargo::rustc-env=BOOTSTRAP_TARGET={target}"); } diff --git a/tools/testsuite/build.rs b/tools/testsuite/build.rs index 44ee884a4..59e9595ec 100644 --- a/tools/testsuite/build.rs +++ b/tools/testsuite/build.rs @@ -25,5 +25,5 @@ use std::env; pub fn main() { let target = env::var("TARGET").expect("cannot find build target"); - println!("cargo:rustc-env=TESTSUITE_TARGET={target}"); + println!("cargo::rustc-env=TESTSUITE_TARGET={target}"); }