pub mod notifications;
pub mod sse;
use super::super::host::ServerHost;
use crate::links::handlers::AppState;
use crate::server::router::build_link_routes;
use anyhow::Result;
use axum::{Json, Router, routing::get};
use serde_json::{Value, json};
use std::sync::Arc;
pub struct RestExposure;
impl RestExposure {
pub fn build_router(host: Arc<ServerHost>, custom_routes: Vec<Router>) -> Result<Router> {
let link_state = AppState {
link_service: host.link_service.clone(),
config: host.config.clone(),
registry: host.registry.clone(),
entity_fetchers: host.entity_fetchers.clone(),
entity_creators: host.entity_creators.clone(),
event_bus: host.event_bus.clone(),
};
let health_routes = Self::health_routes();
let entity_routes = host.entity_registry.build_routes();
let link_routes = build_link_routes(link_state.clone());
let mut app = health_routes.merge(entity_routes);
for custom_router in custom_routes {
app = app.merge(custom_router);
}
app = app.merge(link_routes);
if let Some(event_bus) = &host.event_bus {
let sse_routes = Router::new()
.route("/events/stream", get(sse::sse_handler))
.with_state(event_bus.clone());
app = app.merge(sse_routes);
}
if let (Some(notification_store), Some(preferences_store), Some(device_token_store)) = (
&host.notification_store,
&host.preferences_store,
&host.device_token_store,
) {
let notif_state = notifications::NotificationState {
notification_store: notification_store.clone(),
preferences_store: preferences_store.clone(),
device_token_store: device_token_store.clone(),
};
app = app.merge(notifications::notification_routes(notif_state));
}
Ok(app)
}
fn health_routes() -> Router {
Router::new()
.route("/health", get(Self::health_check))
.route("/healthz", get(Self::health_check))
}
async fn health_check() -> Json<Value> {
Json(json!({
"status": "ok",
"service": "this-rs"
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::LinksConfig;
use crate::server::entity_registry::EntityRegistry;
use crate::server::host::ServerHost;
use crate::storage::InMemoryLinkService;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use std::collections::HashMap;
use tower::ServiceExt;
fn test_host() -> Arc<ServerHost> {
let host = ServerHost::from_builder_components(
Arc::new(InMemoryLinkService::new()),
LinksConfig::default_config(),
EntityRegistry::new(),
HashMap::new(),
HashMap::new(),
)
.expect("should build host");
Arc::new(host)
}
#[test]
fn test_health_routes_builds_router() {
let router = RestExposure::health_routes();
let _ = router;
}
#[tokio::test]
async fn test_health_endpoint_returns_ok() {
let router = RestExposure::health_routes();
let response = router
.oneshot(
Request::builder()
.uri("/health")
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("response should succeed");
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), 1024)
.await
.expect("body should read");
let json: serde_json::Value =
serde_json::from_slice(&body).expect("body should be valid JSON");
assert_eq!(json["status"], "ok");
assert_eq!(json["service"], "this-rs");
}
#[tokio::test]
async fn test_healthz_endpoint_returns_ok() {
let router = RestExposure::health_routes();
let response = router
.oneshot(
Request::builder()
.uri("/healthz")
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("response should succeed");
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), 1024)
.await
.expect("body should read");
let json: serde_json::Value =
serde_json::from_slice(&body).expect("body should be valid JSON");
assert_eq!(json["status"], "ok");
}
#[test]
fn test_build_router_succeeds_with_host() {
let host = test_host();
let router = RestExposure::build_router(host, vec![]);
assert!(router.is_ok());
}
#[test]
fn test_build_router_with_custom_routes() {
use axum::routing::get;
let host = test_host();
let custom = Router::new().route("/custom", get(|| async { "custom" }));
let router = RestExposure::build_router(host, vec![custom]);
assert!(router.is_ok());
}
#[tokio::test]
async fn test_build_router_health_endpoint_reachable() {
let host = test_host();
let router = RestExposure::build_router(host, vec![]).expect("build should succeed");
let response = router
.oneshot(
Request::builder()
.uri("/health")
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("response should succeed");
assert_eq!(response.status(), StatusCode::OK);
}
}