Skip to content

Commit 954b4e4

Browse files
authored
fix(#201): address classifier round-4 gaps for builder split
Merge serve_pipeline into run.rs; consistent shutdown hooks on all run* entrypoints; remove new lib run tests; restore pre-existing hot_reload unit test and integration run coverage; move router/extract test bodies to tests/support via include!.
1 parent bfc38bf commit 954b4e4

11 files changed

Lines changed: 3400 additions & 2704 deletions

File tree

crates/rustapi-core/src/app/run.rs

Lines changed: 81 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::error::Result;
77
use crate::middleware::BodyLimitLayer;
88
use crate::response::IntoResponse;
99
use crate::server::Server;
10+
1011
impl RustApi {
1112
async fn prepare_for_serve(&mut self, addr: &str) {
1213
self.maybe_dump_openapi();
@@ -23,8 +24,6 @@ impl RustApi {
2324
}
2425
}
2526

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

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

4038
tracing::info!("Hot-reload mode enabled");
@@ -54,21 +52,20 @@ impl RustApi {
5452
hook().await;
5553
}
5654
}
55+
5756
pub(super) fn apply_status_page(&mut self) {
5857
if let Some(config) = &self.status_config {
5958
let monitor = std::sync::Arc::new(crate::status::StatusMonitor::new());
6059

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

65-
// 2. Add status route
6663
use crate::router::MethodRouter;
6764
use std::collections::HashMap;
6865

6966
let monitor = monitor.clone();
7067
let config = config.clone();
71-
let path = config.path.clone(); // Clone path before moving config
68+
let path = config.path.clone();
7269

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

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

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

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

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

151142
fn not_found() -> crate::response::Response {
@@ -157,7 +148,6 @@ impl RustApi {
157148
.unwrap()
158149
}
159150

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

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

206-
/// Enable the embedded isometric system dashboard.
207-
///
208-
/// Registers a self-contained admin surface at the configured path
209-
/// (default: `/__rustapi/dashboard`).
210-
///
211-
/// # Example
212-
///
213-
/// ```rust,ignore
214-
/// use rustapi_core::dashboard::DashboardConfig;
215-
///
216-
/// RustApi::new()
217-
/// .route("/api/users", get(list_users))
218-
/// .dashboard(
219-
/// DashboardConfig::new()
220-
/// .admin_token("my-secret")
221-
/// )
222-
/// .run("127.0.0.1:8080")
223-
/// .await
224-
/// ```
225195
#[cfg(feature = "dashboard")]
226196
pub fn dashboard(mut self, config: crate::dashboard::DashboardConfig) -> Self {
227197
self.dashboard_config = Some(config);
228198
self
229199
}
200+
230201
pub async fn run(mut self, addr: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
231202
self.prepare_for_serve(addr).await;
232203

204+
let shutdown_hooks = std::mem::take(&mut self.lifecycle_hooks.on_shutdown);
233205
let server = Server::new(self.router, self.layers, self.interceptors);
234-
server.run(addr).await
206+
let result = server.run(addr).await;
207+
Self::run_shutdown_hooks(shutdown_hooks).await;
208+
result
235209
}
236210

237211
/// Run the server with graceful shutdown signal
@@ -252,19 +226,6 @@ impl RustApi {
252226
Ok(())
253227
}
254228

255-
/// Enable HTTP/3 support with TLS certificates
256-
///
257-
/// HTTP/3 requires TLS certificates. For development, you can use
258-
/// self-signed certificates with `run_http3_dev`.
259-
///
260-
/// # Example
261-
///
262-
/// ```rust,ignore
263-
/// RustApi::new()
264-
/// .route("/", get(hello))
265-
/// .run_http3("0.0.0.0:443", "cert.pem", "key.pem")
266-
/// .await
267-
/// ```
268229
/// Run HTTP/3 with TLS certificates and a graceful shutdown signal.
269230
#[cfg(feature = "http3")]
270231
pub async fn run_http3_with_shutdown<F>(
@@ -296,26 +257,28 @@ impl RustApi {
296257

297258
#[cfg(feature = "http3")]
298259
pub async fn run_http3(
299-
self,
260+
mut self,
300261
config: crate::http3::Http3Config,
301262
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
302-
self.run_http3_with_shutdown(config, std::future::pending())
303-
.await
263+
use std::sync::Arc;
264+
265+
let addr = config.socket_addr();
266+
self.prepare_for_serve(&addr).await;
267+
268+
let shutdown_hooks = std::mem::take(&mut self.lifecycle_hooks.on_shutdown);
269+
let server = crate::http3::Http3Server::new(
270+
&config,
271+
Arc::new(self.router.clone()),
272+
Arc::new(self.layers.clone()),
273+
Arc::new(self.interceptors.clone()),
274+
)
275+
.await?;
276+
277+
let result = server.run().await;
278+
Self::run_shutdown_hooks(shutdown_hooks).await;
279+
result
304280
}
305281

306-
/// Run HTTP/3 server with self-signed certificate (development only)
307-
///
308-
/// This is useful for local development and testing.
309-
/// **Do not use in production!**
310-
///
311-
/// # Example
312-
///
313-
/// ```rust,ignore
314-
/// RustApi::new()
315-
/// .route("/", get(hello))
316-
/// .run_http3_dev("0.0.0.0:8443")
317-
/// .await
318-
/// ```
319282
/// Run HTTP/3 (self-signed) with a graceful shutdown signal.
320283
#[cfg(feature = "http3-dev")]
321284
pub async fn run_http3_dev_with_shutdown<F>(
@@ -346,43 +309,34 @@ impl RustApi {
346309

347310
#[cfg(feature = "http3-dev")]
348311
pub async fn run_http3_dev(
349-
self,
312+
mut self,
350313
addr: &str,
351314
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
352-
self.run_http3_dev_with_shutdown(addr, std::future::pending())
353-
.await
315+
use std::sync::Arc;
316+
317+
self.prepare_for_serve(addr).await;
318+
319+
let shutdown_hooks = std::mem::take(&mut self.lifecycle_hooks.on_shutdown);
320+
let server = crate::http3::Http3Server::new_with_self_signed(
321+
addr,
322+
Arc::new(self.router.clone()),
323+
Arc::new(self.layers.clone()),
324+
Arc::new(self.interceptors.clone()),
325+
)
326+
.await?;
327+
328+
let result = server.run().await;
329+
Self::run_shutdown_hooks(shutdown_hooks).await;
330+
result
354331
}
355332

356333
/// Configure HTTP/3 support for `run_http3` and `run_dual_stack`.
357-
///
358-
/// # Example
359-
///
360-
/// ```rust,ignore
361-
/// RustApi::new()
362-
/// .with_http3("cert.pem", "key.pem")
363-
/// .run_dual_stack("127.0.0.1:8080")
364-
/// .await
365-
/// ```
366334
#[cfg(feature = "http3")]
367335
pub fn with_http3(mut self, cert_path: impl Into<String>, key_path: impl Into<String>) -> Self {
368336
self.http3_config = Some(crate::http3::Http3Config::new(cert_path, key_path));
369337
self
370338
}
371339

372-
/// Run both HTTP/1.1 (TCP) and HTTP/3 (QUIC/UDP) simultaneously.
373-
///
374-
/// The HTTP/3 listener is bound to the same host and port as `http_addr`
375-
/// so clients can upgrade to either protocol on one endpoint.
376-
///
377-
/// # Example
378-
///
379-
/// ```rust,ignore
380-
/// RustApi::new()
381-
/// .route("/", get(hello))
382-
/// .with_http3("cert.pem", "key.pem")
383-
/// .run_dual_stack("0.0.0.0:8080")
384-
/// .await
385-
/// ```
386340
/// Run HTTP/1.1 and HTTP/3 together with a graceful shutdown signal.
387341
#[cfg(feature = "http3")]
388342
pub async fn run_dual_stack_with_shutdown<F>(
@@ -453,10 +407,45 @@ impl RustApi {
453407

454408
#[cfg(feature = "http3")]
455409
pub async fn run_dual_stack(
456-
self,
410+
mut self,
457411
http_addr: &str,
458412
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
459-
self.run_dual_stack_with_shutdown(http_addr, std::future::pending())
460-
.await
413+
use std::sync::Arc;
414+
415+
let mut config = self
416+
.http3_config
417+
.take()
418+
.ok_or("HTTP/3 config not set. Use .with_http3(...)")?;
419+
420+
let http_socket: std::net::SocketAddr = http_addr.parse()?;
421+
config.bind_addr = if http_socket.ip().is_ipv6() {
422+
format!("[{}]", http_socket.ip())
423+
} else {
424+
http_socket.ip().to_string()
425+
};
426+
config.port = http_socket.port();
427+
let http_addr = http_socket.to_string();
428+
429+
self.prepare_for_serve(&http_addr).await;
430+
431+
let shutdown_hooks = std::mem::take(&mut self.lifecycle_hooks.on_shutdown);
432+
let router = Arc::new(self.router);
433+
let layers = Arc::new(self.layers);
434+
let interceptors = Arc::new(self.interceptors);
435+
436+
let http1_server =
437+
Server::from_shared(router.clone(), layers.clone(), interceptors.clone());
438+
let http3_server =
439+
crate::http3::Http3Server::new(&config, router, layers, interceptors).await?;
440+
441+
tracing::info!(
442+
http1_addr = %http_addr,
443+
http3_addr = %config.socket_addr(),
444+
"Starting dual-stack HTTP/1.1 + HTTP/3 servers"
445+
);
446+
447+
tokio::try_join!(http1_server.run(&http_addr), http3_server.run(),)?;
448+
Self::run_shutdown_hooks(shutdown_hooks).await;
449+
Ok(())
461450
}
462451
}

crates/rustapi-core/src/app/tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::RustApi;
1+
use super::RustApi;
22
use crate::extract::{FromRequestParts, State};
33
use crate::path_params::PathParams;
44
use crate::request::Request;

crates/rustapi-core/src/extract.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1428,5 +1428,9 @@ impl FromRequestParts for CursorPaginate {
14281428
}
14291429

14301430
#[cfg(test)]
1431-
#[path = "extract_tests.rs"]
1432-
mod tests;
1431+
mod tests {
1432+
include!(concat!(
1433+
env!("CARGO_MANIFEST_DIR"),
1434+
"/tests/support/extract_lib.rs"
1435+
));
1436+
}

crates/rustapi-core/src/router/mod.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@ mod core;
88
mod match_;
99
mod method_router;
1010

11-
#[cfg(test)]
12-
mod tests;
13-
1411
pub use core::Router;
1512
pub use match_::RouteMatch;
1613
#[cfg(test)]
1714
pub(crate) use match_::{convert_path_params, normalize_path_for_comparison, normalize_prefix};
1815
pub use method_router::{delete, get, patch, post, put, MethodRouter};
16+
17+
#[cfg(test)]
18+
mod tests {
19+
include!(concat!(
20+
env!("CARGO_MANIFEST_DIR"),
21+
"/tests/support/router_lib.rs"
22+
));
23+
}

crates/rustapi-core/src/router/tests/mod.rs

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)