Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 81 additions & 92 deletions crates/rustapi-core/src/app/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::error::Result;
use crate::middleware::BodyLimitLayer;
use crate::response::IntoResponse;
use crate::server::Server;

impl RustApi {
async fn prepare_for_serve(&mut self, addr: &str) {
self.maybe_dump_openapi();
Expand All @@ -23,8 +24,6 @@ impl RustApi {
}
}

/// Returns `None` when hot-reload is disabled; otherwise whether a watcher was
/// already active before this call updated `RUSTAPI_HOT_RELOAD`.
pub(super) fn print_hot_reload_banner(&self, addr: &str) -> Option<bool> {
if !self.hot_reload {
return None;
Expand All @@ -34,7 +33,6 @@ impl RustApi {
.map(|v| v == "1")
.unwrap_or(false);

// Set the env var so the CLI watcher can detect it
std::env::set_var("RUSTAPI_HOT_RELOAD", "1");

tracing::info!("Hot-reload mode enabled");
Expand All @@ -54,21 +52,20 @@ impl RustApi {
hook().await;
}
}

pub(super) fn apply_status_page(&mut self) {
if let Some(config) = &self.status_config {
let monitor = std::sync::Arc::new(crate::status::StatusMonitor::new());

// 1. Add middleware layer
self.layers
.push(Box::new(crate::status::StatusLayer::new(monitor.clone())));

// 2. Add status route
use crate::router::MethodRouter;
use std::collections::HashMap;

let monitor = monitor.clone();
let config = config.clone();
let path = config.path.clone(); // Clone path before moving config
let path = config.path.clone();

let handler: crate::handler::BoxedHandler = std::sync::Arc::new(move |_| {
let monitor = monitor.clone();
Expand All @@ -84,7 +81,6 @@ impl RustApi {
handlers.insert(http::Method::GET, handler);
let method_router = MethodRouter::from_boxed(handlers);

// We need to take the router out to call route() which consumes it
let router = std::mem::take(&mut self.router);
self.router = router.route(&path, method_router);
}
Expand All @@ -104,9 +100,6 @@ impl RustApi {
};
config.normalize_paths();

// Build route inventory from currently registered routes. This snapshot
// intentionally happens before dashboard routes are mounted so the UI
// represents application endpoints rather than the dashboard itself.
let mut inventory: Vec<RouteInventoryItem> = self
.router
.registered_routes()
Expand Down Expand Up @@ -141,11 +134,9 @@ impl RustApi {
config.replay_api_path.clone(),
));

// Insert metrics into router state using the public .state() API
let router = std::mem::take(&mut self.router);
self.router = router.state(std::sync::Arc::clone(&metrics));

// Register dashboard routes
let prefix = config.path.trim_end_matches('/').to_owned();

fn not_found() -> crate::response::Response {
Expand All @@ -157,7 +148,6 @@ impl RustApi {
.unwrap()
}

// Route 1: GET /__rustapi/dashboard (the SPA page)
{
let metrics_c = std::sync::Arc::clone(&metrics);
let config_c = config.clone();
Expand All @@ -179,7 +169,6 @@ impl RustApi {
self.router = router.route(&prefix, MethodRouter::from_boxed(h));
}

// Route 2: GET /__rustapi/dashboard/*path (API sub-paths)
{
let metrics_c = std::sync::Arc::clone(&metrics);
let config_c = config.clone();
Expand All @@ -203,35 +192,20 @@ impl RustApi {
}
}

/// Enable the embedded isometric system dashboard.
///
/// Registers a self-contained admin surface at the configured path
/// (default: `/__rustapi/dashboard`).
///
/// # Example
///
/// ```rust,ignore
/// use rustapi_core::dashboard::DashboardConfig;
///
/// RustApi::new()
/// .route("/api/users", get(list_users))
/// .dashboard(
/// DashboardConfig::new()
/// .admin_token("my-secret")
/// )
/// .run("127.0.0.1:8080")
/// .await
/// ```
#[cfg(feature = "dashboard")]
pub fn dashboard(mut self, config: crate::dashboard::DashboardConfig) -> Self {
self.dashboard_config = Some(config);
self
}

pub async fn run(mut self, addr: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.prepare_for_serve(addr).await;

let shutdown_hooks = std::mem::take(&mut self.lifecycle_hooks.on_shutdown);
let server = Server::new(self.router, self.layers, self.interceptors);
server.run(addr).await
let result = server.run(addr).await;
Self::run_shutdown_hooks(shutdown_hooks).await;
result
}

/// Run the server with graceful shutdown signal
Expand All @@ -252,19 +226,6 @@ impl RustApi {
Ok(())
}

/// Enable HTTP/3 support with TLS certificates
///
/// HTTP/3 requires TLS certificates. For development, you can use
/// self-signed certificates with `run_http3_dev`.
///
/// # Example
///
/// ```rust,ignore
/// RustApi::new()
/// .route("/", get(hello))
/// .run_http3("0.0.0.0:443", "cert.pem", "key.pem")
/// .await
/// ```
/// Run HTTP/3 with TLS certificates and a graceful shutdown signal.
#[cfg(feature = "http3")]
pub async fn run_http3_with_shutdown<F>(
Expand Down Expand Up @@ -296,26 +257,28 @@ impl RustApi {

#[cfg(feature = "http3")]
pub async fn run_http3(
self,
mut self,
config: crate::http3::Http3Config,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.run_http3_with_shutdown(config, std::future::pending())
.await
use std::sync::Arc;

let addr = config.socket_addr();
self.prepare_for_serve(&addr).await;

let shutdown_hooks = std::mem::take(&mut self.lifecycle_hooks.on_shutdown);
let server = crate::http3::Http3Server::new(
&config,
Arc::new(self.router.clone()),
Arc::new(self.layers.clone()),
Arc::new(self.interceptors.clone()),
)
.await?;

let result = server.run().await;
Self::run_shutdown_hooks(shutdown_hooks).await;
result
}

/// Run HTTP/3 server with self-signed certificate (development only)
///
/// This is useful for local development and testing.
/// **Do not use in production!**
///
/// # Example
///
/// ```rust,ignore
/// RustApi::new()
/// .route("/", get(hello))
/// .run_http3_dev("0.0.0.0:8443")
/// .await
/// ```
/// Run HTTP/3 (self-signed) with a graceful shutdown signal.
#[cfg(feature = "http3-dev")]
pub async fn run_http3_dev_with_shutdown<F>(
Expand Down Expand Up @@ -346,43 +309,34 @@ impl RustApi {

#[cfg(feature = "http3-dev")]
pub async fn run_http3_dev(
self,
mut self,
addr: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.run_http3_dev_with_shutdown(addr, std::future::pending())
.await
use std::sync::Arc;

self.prepare_for_serve(addr).await;

let shutdown_hooks = std::mem::take(&mut self.lifecycle_hooks.on_shutdown);
let server = crate::http3::Http3Server::new_with_self_signed(
addr,
Arc::new(self.router.clone()),
Arc::new(self.layers.clone()),
Arc::new(self.interceptors.clone()),
)
.await?;

let result = server.run().await;
Self::run_shutdown_hooks(shutdown_hooks).await;
result
}

/// Configure HTTP/3 support for `run_http3` and `run_dual_stack`.
///
/// # Example
///
/// ```rust,ignore
/// RustApi::new()
/// .with_http3("cert.pem", "key.pem")
/// .run_dual_stack("127.0.0.1:8080")
/// .await
/// ```
#[cfg(feature = "http3")]
pub fn with_http3(mut self, cert_path: impl Into<String>, key_path: impl Into<String>) -> Self {
self.http3_config = Some(crate::http3::Http3Config::new(cert_path, key_path));
self
}

/// Run both HTTP/1.1 (TCP) and HTTP/3 (QUIC/UDP) simultaneously.
///
/// The HTTP/3 listener is bound to the same host and port as `http_addr`
/// so clients can upgrade to either protocol on one endpoint.
///
/// # Example
///
/// ```rust,ignore
/// RustApi::new()
/// .route("/", get(hello))
/// .with_http3("cert.pem", "key.pem")
/// .run_dual_stack("0.0.0.0:8080")
/// .await
/// ```
/// Run HTTP/1.1 and HTTP/3 together with a graceful shutdown signal.
#[cfg(feature = "http3")]
pub async fn run_dual_stack_with_shutdown<F>(
Expand Down Expand Up @@ -453,10 +407,45 @@ impl RustApi {

#[cfg(feature = "http3")]
pub async fn run_dual_stack(
self,
mut self,
http_addr: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.run_dual_stack_with_shutdown(http_addr, std::future::pending())
.await
use std::sync::Arc;

let mut config = self
.http3_config
.take()
.ok_or("HTTP/3 config not set. Use .with_http3(...)")?;

let http_socket: std::net::SocketAddr = http_addr.parse()?;
config.bind_addr = if http_socket.ip().is_ipv6() {
format!("[{}]", http_socket.ip())
} else {
http_socket.ip().to_string()
};
config.port = http_socket.port();
let http_addr = http_socket.to_string();

self.prepare_for_serve(&http_addr).await;

let shutdown_hooks = std::mem::take(&mut self.lifecycle_hooks.on_shutdown);
let router = Arc::new(self.router);
let layers = Arc::new(self.layers);
let interceptors = Arc::new(self.interceptors);

let http1_server =
Server::from_shared(router.clone(), layers.clone(), interceptors.clone());
let http3_server =
crate::http3::Http3Server::new(&config, router, layers, interceptors).await?;

tracing::info!(
http1_addr = %http_addr,
http3_addr = %config.socket_addr(),
"Starting dual-stack HTTP/1.1 + HTTP/3 servers"
);

tokio::try_join!(http1_server.run(&http_addr), http3_server.run(),)?;
Self::run_shutdown_hooks(shutdown_hooks).await;
Ok(())
}
}
2 changes: 1 addition & 1 deletion crates/rustapi-core/src/app/tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::RustApi;
use super::RustApi;
use crate::extract::{FromRequestParts, State};
use crate::path_params::PathParams;
use crate::request::Request;
Expand Down
8 changes: 6 additions & 2 deletions crates/rustapi-core/src/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1428,5 +1428,9 @@ impl FromRequestParts for CursorPaginate {
}

#[cfg(test)]
#[path = "extract_tests.rs"]
mod tests;
mod tests {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/support/extract_lib.rs"
));
}
11 changes: 8 additions & 3 deletions crates/rustapi-core/src/router/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ mod core;
mod match_;
mod method_router;

#[cfg(test)]
mod tests;

pub use core::Router;
pub use match_::RouteMatch;
#[cfg(test)]
pub(crate) use match_::{convert_path_params, normalize_path_for_comparison, normalize_prefix};
pub use method_router::{delete, get, patch, post, put, MethodRouter};

#[cfg(test)]
mod tests {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/support/router_lib.rs"
));
}
3 changes: 0 additions & 3 deletions crates/rustapi-core/src/router/tests/mod.rs

This file was deleted.

Loading
Loading