Skip to content
Clone

feat(webapi): add webapi crate (router, health, openapi)

zhenyi committed
parent 6ea0f7b commit f2ad9b8 Browse files

5 files changed

+176 −0

File tree 5
Added lib/webapi/config.rs View lib/webapi/config.rs
@@ -0,0 +1,32 @@
1use auth::AuthConfig;
2use cache::config::redis::AppRedisSettings;
3use db::config::AppDatabaseSettings;
4use email::config::AppEmailSettings;
5use issues::IssueConfig;
6use jwt::config::JwtSettings;
7use project::ProjectConfig;
8use queue::config::AppQueueSettings;
9use repo::RepoConfig;
10use space::SpaceConfig;
11use storage::config::StorageSettings;
12use user::UserConfig;
13
14/// Bundle of all settings required to construct the full service stack.
15///
16/// Every field provides a sensible `Default`, so the whole bundle can be
17/// derived from environment/config sources and then overridden piecewise.
18#[derive(Clone, Default)]
19pub struct AppConfig {
20 pub db: AppDatabaseSettings,
21 pub cache: AppRedisSettings,
22 pub queue: AppQueueSettings,
23 pub email: AppEmailSettings,
24 pub storage: StorageSettings,
25 pub jwt: JwtSettings,
26 pub auth: AuthConfig,
27 pub issues: IssueConfig,
28 pub project: ProjectConfig,
29 pub repo: RepoConfig,
30 pub space: SpaceConfig,
31 pub user: UserConfig,
32}
Added lib/webapi/health.rs View lib/webapi/health.rs
@@ -0,0 +1,53 @@
1use axum::{Json, http::StatusCode};
2use serde::Serialize;
3use utoipa::ToSchema;
4
5/// Response body for the liveness/readiness probes.
6#[derive(Debug, Serialize, ToSchema)]
7pub struct HealthStatus {
8 pub status: &'static str,
9 pub service: &'static str,
10}
11
12/// Liveness probe: the process is up and able to serve requests.
13#[utoipa::path(
14 get,
15 path = "/health/live",
16 tag = "system",
17 responses(
18 (status = 200, description = "Process is alive", body = HealthStatus)
19 )
20)]
21pub async fn liveness() -> Json<HealthStatus> {
22 Json(HealthStatus {
23 status: "ok",
24 service: "untitled-api",
25 })
26}
27
28/// Readiness probe: the service stack is fully initialised.
29#[utoipa::path(
30 get,
31 path = "/health/ready",
32 tag = "system",
33 responses(
34 (status = 200, description = "Service is ready", body = HealthStatus)
35 )
36)]
37pub async fn readiness() -> Json<HealthStatus> {
38 Json(HealthStatus {
39 status: "ok",
40 service: "untitled-api",
41 })
42}
43
44/// Convenience for probes that want a non-200 signal instead of a JSON body.
45pub fn not_ready() -> (StatusCode, Json<HealthStatus>) {
46 (
47 StatusCode::SERVICE_UNAVAILABLE,
48 Json(HealthStatus {
49 status: "degraded",
50 service: "untitled-api",
51 }),
52 )
53}
Added lib/webapi/lib.rs View lib/webapi/lib.rs
@@ -0,0 +1,15 @@
1//! Unified web API entry crate.
2//!
3//! Composes every domain router (auth, issues, project, repo, space, user)
4//! behind a single router under [`router::API_PREFIX`], alongside health
5//! probes and the merged OpenAPI document.
6
7pub mod config;
8pub mod health;
9pub mod openapi;
10pub mod router;
11pub mod services;
12
13pub use config::AppConfig;
14pub use router::{API_PREFIX, router};
15pub use services::{AppServices, WebapiError, WebapiResult};
Added lib/webapi/openapi.rs View lib/webapi/openapi.rs
@@ -0,0 +1,52 @@
1use axum::http::{Response, StatusCode};
2use utoipa::{Modify, OpenApi};
3use utoipa_scalar::{Scalar, Servable};
4use once_cell::sync::Lazy;
5use crate::{health, router::API_PREFIX};
6
7/// Root OpenAPI document that only carries the health endpoints.
8///
9/// The per-domain documents are merged at runtime by [`openapi`] so that every
10/// path keeps its absolute form (e.g. `/issues/{channel_id}`) before the
11/// unified [`API_PREFIX`] is applied.
12#[derive(OpenApi)]
13#[openapi(
14 info(
15 title = "Untitled API",
16 description = "Unified HTTP API for the untitled platform",
17 version = "1.94.0"
18 ),
19 paths(health::liveness, health::readiness)
20)]
21struct WebapiApiDoc;
22
23/// Build the merged OpenAPI document with every path prefixed by [`API_PREFIX`].
24pub fn openapi() -> utoipa::openapi::OpenApi {
25 let mut doc = export::openapi_concat!(
26 WebapiApiDoc,
27 auth::handler::AuthApiDoc,
28 issues::IssuesApiDoc,
29 project::ProjectApiDoc,
30 repo::handler::openapi::ApiDoc,
31 space::SpaceApiDoc,
32 user::UserApiDoc
33 );
34 export::openapi::PrefixModifier {
35 prefix: API_PREFIX.to_owned(),
36 }
37 .modify(&mut doc);
38 doc
39}
40
41
42static DOCS_HTML: Lazy<String> = Lazy::new(|| {
43 Scalar::with_url("/api-docs", openapi()).to_html()
44});
45
46pub async fn docs() -> Response<String> {
47 Response::builder()
48 .status(StatusCode::OK)
49 .header("Content-Type", "text/html")
50 .body(DOCS_HTML.clone())
51 .expect("Failed to build response")
52}
No newline at end of file
Added lib/webapi/router.rs View lib/webapi/router.rs
@@ -0,0 +1,24 @@
1use axum::{Router, routing::get};
2
3use crate::{health, openapi, services::AppServices};
4
5/// Every API route is mounted below this prefix.
6pub const API_PREFIX: &str = "/api/v1";
7
8/// The unified total router: health probes, OpenAPI docs and every domain router.
9pub fn router(services: AppServices) -> Router {
10 Router::new()
11 .nest(
12 API_PREFIX,
13 Router::new()
14 .route("/health/live", get(health::liveness))
15 .route("/health/ready", get(health::readiness))
16 .merge(auth::auth_router(services.auth))
17 .merge(issues::issues_router(services.issues))
18 .merge(project::project_router(services.project))
19 .merge(repo::repo_router(services.repo))
20 .merge(space::space_router(services.space))
21 .merge(user::user_router(services.user)),
22 )
23 .route("/api-docs", get(openapi::docs))
24}